diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dd0eb21..750e17f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,19 +3,16 @@ on: push: branches: - master - pull_request: - paths: - - '**' -concurrency: - group: lint-${{ github.ref || github.head_ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 jobs: - call-workflow: - uses: kairos-io/linting-composite-action/.github/workflows/reusable-linting.yaml@v0.0.10 - with: - yamldirs: ".github/workflows/ config/" - is-go: true + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 4efda1b..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: 'test' - -on: - push: - branches: - - master - tags: - - '*' - pull_request: - -concurrency: - group: test-${{ github.ref || github.head_ref }} - cancel-in-progress: true - -jobs: - e2e-tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Test - run: | - make kind-e2e-tests - controller-tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Test - run: | - make controller-tests diff --git a/charts/osbuilder/templates/deployment.yaml b/charts/osbuilder/templates/deployment.yaml index 072edf1..890f973 100644 --- a/charts/osbuilder/templates/deployment.yaml +++ b/charts/osbuilder/templates/deployment.yaml @@ -28,6 +28,8 @@ spec: image: '{{ .Values.builder.image.repository | default "ghcr.io/pluralsh/osbuilder" }}:{{ .Values.builder.image.tag | default .Chart.AppVersion }}' command: [ '/manager' ] args: + - --console-url={{ .Values.builder.consoleUrl }} + - --console-token={{ .Values.builder.consoleToken }} - --pvc-storage-size={{ .Values.builder.pvcStorageSize }} - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 diff --git a/charts/osbuilder/values.yaml b/charts/osbuilder/values.yaml index 26288ab..77bde81 100644 --- a/charts/osbuilder/values.yaml +++ b/charts/osbuilder/values.yaml @@ -16,6 +16,9 @@ builder: tag: ~ replicas: 1 + consoleUrl: "" + consoleToken: "" + # The PVC storage size for the build process pvcStorageSize: "30Gi" diff --git a/controllers/osartifact_controller.go b/controllers/osartifact_controller.go index c1597af..699bcb0 100644 --- a/controllers/osartifact_controller.go +++ b/controllers/osartifact_controller.go @@ -21,13 +21,14 @@ import ( "fmt" "time" - "k8s.io/apimachinery/pkg/api/meta" - - "k8s.io/apimachinery/pkg/api/errors" - + osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2" + consoleclient "github.com/kairos-io/osbuilder/pkg/client" + console "github.com/pluralsh/console/go/client" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -40,8 +41,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2" ) const ( @@ -60,6 +59,7 @@ var ( // OSArtifactReconciler reconciles a OSArtifact object type OSArtifactReconciler struct { client.Client + ConsoleClient consoleclient.Client Scheme *runtime.Scheme ServingImage, ToolImage, CopierImage, PVCStorage string } @@ -404,8 +404,18 @@ func (r *OSArtifactReconciler) checkExport(ctx context.Context, artifact *osbuil } } else if job.Spec.Completions != nil { - if job.Status.Succeeded > 0 { + if job.Status.Succeeded > 0 && artifact.Status.Phase == osbuilder.Exporting { artifact.Status.Phase = osbuilder.Ready + if err := r.upsertClusterIsoImage(artifact); err != nil { + artifact.Status.Phase = osbuilder.Error + meta.SetStatusCondition(&artifact.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: consoleclient.GetErrorResponse(err, "CreateClusterIsoImage").Error(), + }) + } + if err := TryToUpdateStatus(ctx, r.Client, artifact); err != nil { log.FromContext(ctx).Error(err, "failed to update artifact status") return ctrl.Result{}, err @@ -434,6 +444,27 @@ func (r *OSArtifactReconciler) checkExport(ctx context.Context, artifact *osbuil return requeue, nil } +func (r *OSArtifactReconciler) upsertClusterIsoImage(artifact *osbuilder.OSArtifact) error { + image := fmt.Sprintf("%s:%s", artifact.Spec.Exporter.Registry.Image.Repository, artifact.Spec.Exporter.Registry.Image.Tag) + attr := console.ClusterIsoImageAttributes{ + Image: image, + Registry: artifact.Spec.Exporter.Registry.Name, + } + + getResponse, err := r.ConsoleClient.GetClusterIsoImage(&image) + if err != nil { + if errors.IsNotFound(err) { + _, err := r.ConsoleClient.CreateClusterIsoImage(attr) + return err + } + return err + } + if _, err := r.ConsoleClient.UpdateClusterIsoImage(getResponse.ID, attr); err != nil { + return err + } + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *OSArtifactReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/go.mod b/go.mod index 3447323..bfcc9c8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/kairos-io/osbuilder go 1.23.4 require ( + github.com/Yamashou/gqlgenc v0.23.2 + github.com/pkg/errors v0.9.1 + github.com/pluralsh/console/go/client v1.28.3 k8s.io/api v0.32.1 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 @@ -11,6 +14,7 @@ require ( ) require ( + github.com/99designs/gqlgen v0.17.49 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -39,12 +43,13 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/gomega v1.36.2 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/vektah/gqlparser/v2 v2.5.16 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/go.sum b/go.sum index 1786f83..b2c8999 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ= +github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0= +github.com/Yamashou/gqlgenc v0.23.2 h1:WPxYPrwc6W4Z1eY4qKxoH3nb5PC4jAMWqQA0G8toQMI= +github.com/Yamashou/gqlgenc v0.23.2/go.mod h1:oMc4EQBQeDwLIODvgcvpaSp6rO+KMf47FuOhplv5D3A= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -77,6 +83,8 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pluralsh/console/go/client v1.28.3 h1:KyHpOoVHhMRMAZp99k7LqZqX6+FrOwy9sGVXaDSi9FA= +github.com/pluralsh/console/go/client v1.28.3/go.mod h1:lpoWASYsM9keNePS3dpFiEisUHEfObIVlSL3tzpKn8k= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -90,6 +98,10 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -101,6 +113,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= +github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/main.go b/main.go index c844e8f..779138e 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,8 @@ import ( "flag" "os" + consoleclient "github.com/kairos-io/osbuilder/pkg/client" + "github.com/kairos-io/osbuilder/pkg/helpers" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -43,6 +45,8 @@ var ( setupLog = ctrl.Log.WithName("setup") ) +const EnvConsoleToken = "CONSOLE_TOKEN" + func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) @@ -55,6 +59,10 @@ func main() { var enableLeaderElection bool var probeAddr string var serveImage, toolImage, copierImage, pvcStorage string + var consoleUrl, consoleToken string + + flag.StringVar(&consoleUrl, "console-url", "", "The URL of the console api to fetch services from.") + flag.StringVar(&consoleToken, "console-token", helpers.GetEnv(EnvConsoleToken, ""), "The deploy token to auth to Console API with.") flag.StringVar(&pvcStorage, "pvc-storage-size", "20Gi", "The PVC storage size for building process") @@ -75,6 +83,8 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + extConsoleClient := consoleclient.New(consoleUrl, consoleToken) + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddr}, @@ -99,12 +109,13 @@ func main() { } if err = (&controllers.OSArtifactReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ServingImage: serveImage, - ToolImage: toolImage, - CopierImage: copierImage, - PVCStorage: pvcStorage, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ServingImage: serveImage, + ToolImage: toolImage, + CopierImage: copierImage, + PVCStorage: pvcStorage, + ConsoleClient: extConsoleClient, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "OSArtifact") os.Exit(1) diff --git a/pkg/client/console.go b/pkg/client/console.go new file mode 100644 index 0000000..c4e1f00 --- /dev/null +++ b/pkg/client/console.go @@ -0,0 +1,108 @@ +package client + +import ( + "context" + "encoding/json" + "net/http" + + rawclient "github.com/Yamashou/gqlgenc/clientv2" + internalerror "github.com/kairos-io/osbuilder/pkg/errors" + "github.com/kairos-io/osbuilder/pkg/helpers" + "github.com/pkg/errors" + console "github.com/pluralsh/console/go/client" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type client struct { + ctx context.Context + consoleClient console.ConsoleClient + url string + token string +} + +func New(url, token string) Client { + return &client{ + consoleClient: console.NewClient(&http.Client{ + Transport: helpers.NewAuthorizationTokenTransport(token), + }, url, nil), + ctx: context.Background(), + url: url, + token: token, + } +} + +type Client interface { + CreateClusterIsoImage(attributes console.ClusterIsoImageAttributes) (*console.ClusterIsoImageFragment, error) + UpdateClusterIsoImage(id string, attributes console.ClusterIsoImageAttributes) (*console.ClusterIsoImageFragment, error) + GetClusterIsoImage(image *string) (*console.ClusterIsoImageFragment, error) + DeleteClusterIsoImage(id string) (*console.ClusterIsoImageFragment, error) +} + +func (c *client) CreateClusterIsoImage(attributes console.ClusterIsoImageAttributes) (*console.ClusterIsoImageFragment, error) { + response, err := c.consoleClient.CreateClusterIsoImage(c.ctx, attributes) + if err != nil { + return nil, err + } + + return response.CreateClusterIsoImage, nil +} + +func (c *client) DeleteClusterIsoImage(id string) (*console.ClusterIsoImageFragment, error) { + response, err := c.consoleClient.DeleteClusterIsoImage(c.ctx, id) + if err != nil { + return nil, err + } + + return response.DeleteClusterIsoImage, nil +} + +func (c *client) UpdateClusterIsoImage(id string, attributes console.ClusterIsoImageAttributes) (*console.ClusterIsoImageFragment, error) { + response, err := c.consoleClient.UpdateClusterIsoImage(c.ctx, id, attributes) + if err != nil { + return nil, err + } + + return response.UpdateClusterIsoImage, nil +} + +func (c *client) GetClusterIsoImage(image *string) (*console.ClusterIsoImageFragment, error) { + response, err := c.consoleClient.GetClusterIsoImage(c.ctx, nil, image) + if internalerror.IsNotFound(err) { + return nil, apierrors.NewNotFound(schema.GroupResource{}, *image) + } + if err == nil && (response == nil || response.ClusterIsoImage == nil) { + return nil, apierrors.NewNotFound(schema.GroupResource{}, *image) + } + + if response == nil { + return nil, err + } + + return response.ClusterIsoImage, nil +} + +func GetErrorResponse(err error, methodName string) error { + if err == nil { + return nil + } + errResponse := &rawclient.ErrorResponse{} + newErr := json.Unmarshal([]byte(err.Error()), errResponse) + if newErr != nil { + return err + } + + errList := errors.New(methodName) + if errResponse.GqlErrors != nil { + for _, err := range *errResponse.GqlErrors { + errList = errors.Wrap(errList, err.Message) + } + errList = errors.Wrap(errList, "GraphQL error") + } + if errResponse.NetworkError != nil { + errList = errors.Wrap(errList, errResponse.NetworkError.Message) + errList = errors.Wrap(errList, "Network error") + } + + return errList +} diff --git a/pkg/errors/base.go b/pkg/errors/base.go new file mode 100644 index 0000000..0d5c0cb --- /dev/null +++ b/pkg/errors/base.go @@ -0,0 +1,59 @@ +package errors + +import ( + "errors" + + client "github.com/Yamashou/gqlgenc/clientv2" +) + +type KnownError string + +func (k KnownError) String() string { + return string(k) +} + +func (k KnownError) Error() string { + return string(k) +} + +const ( + ErrorNotFound KnownError = "could not find resource" +) + +type wrappedErrorResponse struct { + err *client.ErrorResponse +} + +func (er *wrappedErrorResponse) Has(err KnownError) bool { + if er.err.GqlErrors == nil { + return false + } + + for _, g := range *er.err.GqlErrors { + if g.Message == string(err) { + return true + } + } + + return false +} + +func newAPIError(err *client.ErrorResponse) *wrappedErrorResponse { + return &wrappedErrorResponse{ + err: err, + } +} + +func IsNotFound(err error) bool { + if err == nil { + return false + } + + errorResponse := new(client.ErrorResponse) + ok := errors.As(err, &errorResponse) + if !ok { + return false + } + + return newAPIError(errorResponse).Has(ErrorNotFound) +} diff --git a/pkg/helpers/env.go b/pkg/helpers/env.go new file mode 100644 index 0000000..d834d35 --- /dev/null +++ b/pkg/helpers/env.go @@ -0,0 +1,14 @@ +package helpers + +import ( + "os" +) + +// GetEnv - Lookup the environment variable provided and set to default value if variable isn't found +func GetEnv(key, fallback string) string { + if value := os.Getenv(key); len(value) > 0 { + return value + } + + return fallback +} diff --git a/pkg/helpers/http.go b/pkg/helpers/http.go new file mode 100644 index 0000000..37681c3 --- /dev/null +++ b/pkg/helpers/http.go @@ -0,0 +1,33 @@ +package helpers + +import ( + "net/http" +) + +type AuthorizationTokenTransport struct { + token string + transport http.RoundTripper +} + +func (in *AuthorizationTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Token "+in.token) + return in.transport.RoundTrip(req) +} + +func NewAuthorizationTokenTransport(token string) http.RoundTripper { + return &AuthorizationTokenTransport{token: token, transport: http.DefaultTransport} +} + +type AuthorizationBearerTransport struct { + token string + transport http.RoundTripper +} + +func (in *AuthorizationBearerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+in.token) + return in.transport.RoundTrip(req) +} + +func NewAuthorizationBearerTransport(token string) http.RoundTripper { + return &AuthorizationBearerTransport{token: token, transport: http.DefaultTransport} +}