Some checks failed
Go Tests / go-tests (push) Failing after 2m40s
Updates edge-connect-client dependency to use the new SDK package and adapts provider code to use the updated client interfaces. Key changes: - Switches from direct client usage to SDK-based implementation - Updates dependency versions for K8s and other packages - Improves HTTP client configuration with timeout settings - Maintains existing functionality while modernizing implementation This change improves code maintainability and reliability through the use of the official SDK package.
514 lines
14 KiB
Go
514 lines
14 KiB
Go
// Copyright 2023 Cloudbase Solutions SRL
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
|
"edp.buildth.ing/DevFW-CICD/garm-provider-edge-connect/config"
|
|
"edp.buildth.ing/DevFW-CICD/garm-provider-edge-connect/internal/spec"
|
|
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/ptr"
|
|
|
|
execution "github.com/cloudbase/garm-provider-common/execution/v0.1.0"
|
|
"github.com/cloudbase/garm-provider-common/params"
|
|
)
|
|
|
|
var Version = "v0.0.0-unknown"
|
|
|
|
type GitHubScopeDetails struct {
|
|
BaseURL string
|
|
Repo string
|
|
Org string
|
|
Enterprise string
|
|
}
|
|
|
|
func NewEdgeConnectProvider(configPath, controllerID string) (execution.ExternalProvider, string, error) {
|
|
conf, err := config.NewConfig(configPath)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error loading config: %w", err)
|
|
}
|
|
|
|
creds, err := config.NewCredentials(conf.CredentialsFile)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error loading config: %w", err)
|
|
}
|
|
|
|
client := edgeconnect.NewClientWithCredentials(
|
|
"https://hub.apps.edge.platform.mg3.mdb.osc.live",
|
|
creds.Username,
|
|
creds.Password,
|
|
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
|
)
|
|
|
|
return &edgeConnectProvider{
|
|
client: client,
|
|
controllerID: controllerID,
|
|
cfg: conf,
|
|
}, conf.LogFile, nil
|
|
}
|
|
|
|
type edgeConnectProvider struct {
|
|
client *edgeconnect.Client
|
|
controllerID string
|
|
cfg *config.Config
|
|
}
|
|
|
|
// CreateInstance creates a new compute instance in the provider.
|
|
func (a *edgeConnectProvider) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.ProviderInstance, error) {
|
|
log.Printf("Executing CreateInstance with %v\n", bootstrapParams)
|
|
log.Printf("Executing CreateInstance with RepoURL %v\n", bootstrapParams.RepoURL)
|
|
|
|
instancename := fmt.Sprintf("garm-%v-%v-%v", a.controllerID[:8], bootstrapParams.PoolID[:8], strings.ToLower(bootstrapParams.Name))
|
|
|
|
gitHubScopeDetails, err := spec.ExtractGitHubScopeDetails(bootstrapParams.RepoURL)
|
|
if err != nil {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
|
|
envs := spec.GetRunnerEnvs(gitHubScopeDetails, bootstrapParams)
|
|
|
|
jobv1 := batchv1.Job{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Job",
|
|
APIVersion: "batch/v1",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: instancename,
|
|
Labels: map[string]string{"run": instancename},
|
|
},
|
|
Spec: batchv1.JobSpec{
|
|
BackoffLimit: ptr.To(int32(0)),
|
|
Completions: ptr.To(int32(1)),
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
RestartPolicy: "Never",
|
|
Containers: []corev1.Container{
|
|
corev1.Container{
|
|
Name: "mganter-test",
|
|
Image: "edp.buildth.ing/devfw-cicd/garm-act-runner:1",
|
|
ImagePullPolicy: "Always",
|
|
Env: envs,
|
|
VolumeMounts: []corev1.VolumeMount{
|
|
corev1.VolumeMount{
|
|
MountPath: "/runner",
|
|
Name: "cache-volume",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Volumes: []corev1.Volume{
|
|
corev1.Volume{
|
|
Name: "cache-volume",
|
|
VolumeSource: corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
manifest, err := json.Marshal(jobv1)
|
|
if err != nil {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
|
|
_, err = a.client.ShowApp(ctx, edgeconnect.AppKey{
|
|
Organization: a.cfg.Organization,
|
|
Name: instancename,
|
|
Version: "0.0.1",
|
|
}, a.cfg.Region)
|
|
if err != nil && !errors.Is(err, edgeconnect.ErrResourceNotFound) {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
if errors.Is(err, edgeconnect.ErrResourceNotFound) {
|
|
err = a.client.CreateApp(ctx, &edgeconnect.NewAppInput{
|
|
Region: a.cfg.Region,
|
|
App: edgeconnect.App{
|
|
Key: edgeconnect.AppKey{
|
|
Organization: a.cfg.Organization,
|
|
Name: instancename,
|
|
Version: "0.0.1",
|
|
},
|
|
Deployment: "kubernetes",
|
|
ImageType: "Docker",
|
|
ImagePath: "edp.buildth.ing/devfw-cicd/garm-act-runner:1",
|
|
AllowServerless: true,
|
|
ServerlessConfig: struct{}{},
|
|
DefaultFlavor: edgeconnect.Flavor{
|
|
Name: "EU.small",
|
|
},
|
|
DeploymentGenerator: "kubernetes-basic",
|
|
DeploymentManifest: string(manifest),
|
|
RequiredOutboundConnections: []edgeconnect.SecurityRule{
|
|
edgeconnect.SecurityRule{
|
|
PortRangeMax: 65535,
|
|
PortRangeMin: 1,
|
|
Protocol: "TCP",
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
},
|
|
edgeconnect.SecurityRule{
|
|
PortRangeMax: 65535,
|
|
PortRangeMin: 1,
|
|
Protocol: "UDP",
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
}
|
|
|
|
_, err = a.client.ShowAppInstance(ctx, edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
Name: instancename,
|
|
CloudletKey: edgeconnect.CloudletKey(a.cfg.CloudletKey),
|
|
}, a.cfg.Region)
|
|
if err != nil && !errors.Is(err, edgeconnect.ErrResourceNotFound) {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
if errors.Is(err, edgeconnect.ErrResourceNotFound) {
|
|
err = a.client.CreateAppInstance(ctx, &edgeconnect.NewAppInstanceInput{
|
|
Region: a.cfg.Region,
|
|
AppInst: edgeconnect.AppInstance{
|
|
Key: edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
Name: instancename,
|
|
CloudletKey: edgeconnect.CloudletKey(a.cfg.CloudletKey),
|
|
},
|
|
AppKey: edgeconnect.AppKey{
|
|
Organization: a.cfg.Organization,
|
|
Name: instancename,
|
|
Version: "0.0.1",
|
|
},
|
|
Flavor: edgeconnect.Flavor{
|
|
Name: "EU.small",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
}
|
|
|
|
instance := params.ProviderInstance{
|
|
ProviderID: instancename,
|
|
Name: instancename,
|
|
OSType: params.Linux,
|
|
OSArch: params.Amd64,
|
|
OSName: "lala",
|
|
OSVersion: "lalatest",
|
|
Status: params.InstanceRunning,
|
|
}
|
|
|
|
return instance, nil
|
|
}
|
|
|
|
func GetRunnerEnvs(gitHubScope GitHubScopeDetails, bootstrapParams params.BootstrapInstance) []corev1.EnvVar {
|
|
return []corev1.EnvVar{
|
|
{
|
|
Name: "RUNNER_ORG",
|
|
Value: gitHubScope.Org,
|
|
},
|
|
{
|
|
Name: "RUNNER_REPO",
|
|
Value: gitHubScope.Repo,
|
|
},
|
|
{
|
|
Name: "RUNNER_ENTERPRISE",
|
|
Value: gitHubScope.Enterprise,
|
|
},
|
|
{
|
|
Name: "RUNNER_GROUP",
|
|
Value: bootstrapParams.GitHubRunnerGroup,
|
|
},
|
|
{
|
|
Name: "RUNNER_NAME",
|
|
Value: bootstrapParams.Name,
|
|
},
|
|
{
|
|
Name: "RUNNER_LABELS",
|
|
Value: strings.Join(bootstrapParams.Labels, ","),
|
|
},
|
|
{
|
|
Name: "RUNNER_NO_DEFAULT_LABELS",
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "DISABLE_RUNNER_UPDATE",
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "RUNNER_WORKDIR",
|
|
Value: "/runner/_work/",
|
|
},
|
|
{
|
|
Name: "GITHUB_URL",
|
|
Value: gitHubScope.BaseURL,
|
|
},
|
|
{
|
|
Name: "RUNNER_EPHEMERAL",
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "RUNNER_TOKEN",
|
|
Value: "dummy",
|
|
},
|
|
{
|
|
Name: "METADATA_URL",
|
|
Value: bootstrapParams.MetadataURL,
|
|
},
|
|
{
|
|
Name: "BEARER_TOKEN",
|
|
Value: bootstrapParams.InstanceToken,
|
|
},
|
|
{
|
|
Name: "CALLBACK_URL",
|
|
Value: bootstrapParams.CallbackURL,
|
|
},
|
|
{
|
|
Name: "JIT_CONFIG_ENABLED",
|
|
Value: fmt.Sprintf("%t", bootstrapParams.JitConfigEnabled),
|
|
},
|
|
}
|
|
}
|
|
|
|
func ExtractGitHubScopeDetails(gitRepoURL string) (GitHubScopeDetails, error) {
|
|
if gitRepoURL == "" {
|
|
return GitHubScopeDetails{}, fmt.Errorf("no gitRepoURL supplied")
|
|
}
|
|
u, err := url.Parse(gitRepoURL)
|
|
if err != nil {
|
|
return GitHubScopeDetails{}, fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
|
|
if u.Scheme == "" || u.Host == "" {
|
|
return GitHubScopeDetails{}, fmt.Errorf("invalid URL: %s", gitRepoURL)
|
|
}
|
|
|
|
pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
|
|
|
scope := GitHubScopeDetails{
|
|
BaseURL: u.Scheme + "://" + u.Host,
|
|
}
|
|
|
|
switch {
|
|
case len(pathParts) == 1:
|
|
scope.Org = pathParts[0]
|
|
case len(pathParts) == 2 && pathParts[0] == "enterprises":
|
|
scope.Enterprise = pathParts[1]
|
|
case len(pathParts) == 2:
|
|
scope.Org = pathParts[0]
|
|
scope.Repo = pathParts[1]
|
|
default:
|
|
return GitHubScopeDetails{}, fmt.Errorf("URL does not match the expected patterns")
|
|
}
|
|
|
|
return scope, nil
|
|
}
|
|
|
|
// Delete instance will delete the instance in a provider.
|
|
func (a *edgeConnectProvider) DeleteInstance(ctx context.Context, instance string) error {
|
|
log.Printf("Executing DeleteInstance %s\n", instance)
|
|
|
|
appsinstances, err := a.client.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
}, a.cfg.Region)
|
|
if err != nil {
|
|
log.Printf("Error in method DeleteInstance() ShowAppInstances")
|
|
return err
|
|
}
|
|
|
|
log.Printf("got %d appinstances", len(appsinstances))
|
|
|
|
myappintances := filter(appsinstances, func(app edgeconnect.AppInstance) bool {
|
|
return strings.HasSuffix(app.Key.Name, strings.ToLower(instance))
|
|
})
|
|
|
|
log.Printf("filtered %d appinstances", len(myappintances))
|
|
|
|
for _, v := range myappintances {
|
|
err = a.client.DeleteAppInstance(ctx, v.Key, a.cfg.Region)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
apps, err := a.client.ShowApps(ctx, edgeconnect.AppKey{
|
|
Organization: a.cfg.Organization,
|
|
}, a.cfg.Region)
|
|
if err != nil {
|
|
log.Printf("Error in method DeleteInstance() ShowApps")
|
|
return err
|
|
}
|
|
|
|
myapps := filter(apps, func(app edgeconnect.App) bool {
|
|
return strings.HasSuffix(app.Key.Name, strings.ToLower(instance))
|
|
})
|
|
|
|
for _, v := range myapps {
|
|
err = a.client.DeleteApp(ctx, v.Key, a.cfg.Region)
|
|
if err != nil {
|
|
log.Printf("Error in method DeleteInstance() DeleteApp")
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetInstance will return details about one instance.
|
|
func (a *edgeConnectProvider) GetInstance(ctx context.Context, instance string) (params.ProviderInstance, error) {
|
|
log.Printf("Executing GetInstance %s\n", instance)
|
|
providerInstance := params.ProviderInstance{
|
|
ProviderID: a.controllerID,
|
|
Name: instance,
|
|
OSType: params.Linux,
|
|
OSArch: params.Amd64,
|
|
OSName: "lala",
|
|
OSVersion: "lalatest",
|
|
Status: params.InstanceStatusUnknown,
|
|
}
|
|
|
|
appinstances, err := a.client.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
}, a.cfg.Region)
|
|
if err != nil {
|
|
return params.ProviderInstance{}, err
|
|
}
|
|
|
|
myappintances := filter(appinstances, func(app edgeconnect.AppInstance) bool {
|
|
return strings.HasSuffix(app.Key.Name, strings.ToLower(instance))
|
|
})
|
|
|
|
if len(myappintances) == 0 {
|
|
return params.ProviderInstance{}, fmt.Errorf("AppInstance not found!")
|
|
}
|
|
|
|
appinst := myappintances[0]
|
|
|
|
if appinst.State == "Ready" {
|
|
providerInstance.Status = params.InstanceRunning
|
|
}
|
|
|
|
return providerInstance, nil
|
|
}
|
|
|
|
// ListInstances will list all instances for a provider.
|
|
func (a *edgeConnectProvider) ListInstances(ctx context.Context, poolID string) ([]params.ProviderInstance, error) {
|
|
log.Printf("Executing ListInstances for PoolID %s\n", poolID)
|
|
|
|
apps, err := a.client.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
}, a.cfg.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
myappintances := filter(apps, func(app edgeconnect.AppInstance) bool {
|
|
return strings.HasPrefix(app.Key.Name, fmt.Sprintf("garm-%v-%v", a.controllerID[:8], poolID[:8]))
|
|
})
|
|
|
|
providerinstances := []params.ProviderInstance{}
|
|
|
|
for _, v := range myappintances {
|
|
providerInstance := params.ProviderInstance{
|
|
ProviderID: a.controllerID,
|
|
Name: v.Key.Name,
|
|
OSType: params.Linux,
|
|
OSArch: params.Amd64,
|
|
OSName: "lala",
|
|
OSVersion: "lalatest",
|
|
Status: params.InstanceStatusUnknown,
|
|
}
|
|
|
|
if v.State == "Ready" {
|
|
providerInstance.Status = params.InstanceRunning
|
|
}
|
|
|
|
providerinstances = append(providerinstances, providerInstance)
|
|
}
|
|
|
|
return providerinstances, nil
|
|
}
|
|
|
|
func filter[T any](s []T, predicate func(T) bool) []T {
|
|
result := make([]T, 0, len(s)) // Pre-allocate for efficiency
|
|
for _, v := range s {
|
|
if predicate(v) {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// RemoveAllInstances will remove all instances created by this provider.
|
|
func (a *edgeConnectProvider) RemoveAllInstances(ctx context.Context) error {
|
|
log.Printf("Executing RemoveAllInstances\n")
|
|
|
|
apps, err := a.client.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{
|
|
Organization: a.cfg.Organization,
|
|
}, a.cfg.Region)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
myappintances := filter(apps, func(app edgeconnect.AppInstance) bool {
|
|
return strings.HasPrefix(app.Key.Name, fmt.Sprintf("garm-%v", a.controllerID[:8]))
|
|
})
|
|
|
|
for _, v := range myappintances {
|
|
err = a.DeleteInstance(ctx, v.Key.Name)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop shuts down the instance.
|
|
func (a *edgeConnectProvider) Stop(ctx context.Context, instance string, force bool) error {
|
|
log.Printf("Executing Stop %s\n", instance)
|
|
panic(fmt.Sprintf("Stop() not implemented, called with instance: %s, force: %t", instance, force))
|
|
}
|
|
|
|
// Start boots up an instance.
|
|
func (a *edgeConnectProvider) Start(ctx context.Context, instance string) error {
|
|
log.Printf("Executing Start %s\n", instance)
|
|
panic(fmt.Sprintf("Start() not implemented, called with instance: %s", instance))
|
|
}
|
|
|
|
// GetVersion returns the version of the provider.
|
|
func (a *edgeConnectProvider) GetVersion(ctx context.Context) string {
|
|
return Version
|
|
}
|