create cluster ISO image

This commit is contained in:
Lukasz Zajaczkowski
2025-02-06 14:29:01 +01:00
parent 0ecfe9672a
commit 390891fa14
12 changed files with 305 additions and 60 deletions

View File

@@ -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
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
yamldirs: ".github/workflows/ config/"
is-go: true
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v6

View File

@@ -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

View File

@@ -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

View File

@@ -16,6 +16,9 @@ builder:
tag: ~
replicas: 1
consoleUrl: ""
consoleToken: ""
# The PVC storage size for the build process
pvcStorageSize: "30Gi"

View File

@@ -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).

7
go.mod
View File

@@ -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

14
go.sum
View File

@@ -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=

11
main.go
View File

@@ -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},
@@ -105,6 +115,7 @@ func main() {
ToolImage: toolImage,
CopierImage: copierImage,
PVCStorage: pvcStorage,
ConsoleClient: extConsoleClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OSArtifact")
os.Exit(1)

108
pkg/client/console.go Normal file
View File

@@ -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
}

59
pkg/errors/base.go Normal file
View File

@@ -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)
}

14
pkg/helpers/env.go Normal file
View File

@@ -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
}

33
pkg/helpers/http.go Normal file
View File

@@ -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}
}