Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ func main() { // nolint:gocyclo

// create or update all NSTemplateTiers on the cluster at startup
setupLog.Info("Creating/updating the NSTemplateTier resources")
if err := nstemplatetiers.CreateOrUpdateResources(ctx, mgr.GetScheme(), mgr.GetClient(), namespace); err != nil {
if err := nstemplatetiers.SyncResources(ctx, mgr.GetScheme(), mgr.GetClient(), namespace); err != nil {
setupLog.Error(err, "")
os.Exit(1)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/codeready-toolchain/host-operator
require (
cloud.google.com/go/recaptchaenterprise/v2 v2.13.0
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12
github.com/codeready-toolchain/toolchain-common v0.0.0-20250708104334-c3ec13e2e8da
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.2
github.com/gofrs/uuid v4.4.0+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZ
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27 h1:g1ivSPPHTC96RHp8S/gRmqODWgoHyivq+/d5kSI0pEs=
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27/go.mod h1:20258od6i5+jP406Z76YaI2ow/vc7URwsDU2bokpkRE=
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12 h1:w54sojJJ8PsHZzK1mC+/EUBrQ9F2sC/k7JUVc8LSqK4=
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12/go.mod h1:TrMvD0sP69wI6Rouzfs7OsOUSj4CGn/ZiIdiDBAFQjk=
github.com/codeready-toolchain/toolchain-common v0.0.0-20250708104334-c3ec13e2e8da h1:5V3vJKbhDViUwC5M3jN57Us4lFls/wbtV9xmSsXjONQ=
github.com/codeready-toolchain/toolchain-common v0.0.0-20250708104334-c3ec13e2e8da/go.mod h1:mjwK6D+gH299P2CEUgliLEwJs4K+Cx++Bns/8rYkxUU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
9 changes: 9 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package constants

// HostOperatorFieldManager is the field manager we want to use in the managed fields
// of objects deployed by the host operator.
const HostOperatorFieldManager = "kubesaw-host-operator"

// BundledWithHostOperatorAnnotationValue is meant to be the value of the toolchainv1alpha1.BundledLabelKey that marks
// the objects as bundled with the host operator and therefore managed by it.
const BundledWithHostOperatorAnnotationValue = "host-operator"
5 changes: 0 additions & 5 deletions pkg/constants/field_manager.go

This file was deleted.

67 changes: 51 additions & 16 deletions pkg/templates/nstemplatetiers/nstemplatetier_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,78 @@
import (
"context"
"embed"
"errors"
"fmt"
"path/filepath"
"slices"
"strings"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/host-operator/deploy"
"github.com/codeready-toolchain/host-operator/pkg/constants"
"github.com/codeready-toolchain/host-operator/pkg/templates"
commonclient "github.com/codeready-toolchain/toolchain-common/pkg/client"
"github.com/codeready-toolchain/toolchain-common/pkg/template/nstemplatetiers"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

const NsTemplateTierRootDir = "templates/nstemplatetiers"

// CreateOrUpdateResources generates the NSTemplateTier resources from the cluster resource template and namespace templates,
// then uses the manager's client to create or update the resources on the cluster.
func CreateOrUpdateResources(ctx context.Context, s *runtime.Scheme, client runtimeclient.Client, namespace string) error {
var bundledAnnotation = map[string]string{
toolchainv1alpha1.BundledAnnotationKey: constants.BundledWithHostOperatorAnnotationValue,
}

// SyncResources generates the NSTemplateTier resources from the cluster resource template and namespace templates,
// then uses the manager's client to create or update the resources on the cluster. It also deletes all the tiers
// that used to be bundled but are not anymore.
func SyncResources(ctx context.Context, s *runtime.Scheme, client runtimeclient.Client, namespace string) error {
metadata, files, err := LoadFiles(deploy.NSTemplateTiersFS, NsTemplateTierRootDir)
if err != nil {
return err
}

var bundledTierKeys []runtimeclient.ObjectKey

// initialize tier generator, loads templates from assets
return nstemplatetiers.GenerateTiers(s, func(toEnsure runtimeclient.Object, canUpdate bool, _ string) (bool, error) {
if !canUpdate {
if err := client.Create(ctx, toEnsure); err != nil && !apierrors.IsAlreadyExists(err) {
return false, err
err = nstemplatetiers.GenerateTiers(s, func(toEnsure runtimeclient.Object, _ string) error {
commonclient.MergeAnnotations(toEnsure, bundledAnnotation)

bundledTierKeys = append(bundledTierKeys, runtimeclient.ObjectKeyFromObject(toEnsure))

applyCl := commonclient.NewSSAApplyClient(client, constants.HostOperatorFieldManager)
return applyCl.ApplyObject(ctx, toEnsure)
}, namespace, metadata, files)
if err != nil {
return err
}

return removeNoLongerBundledTiers(ctx, client, namespace, bundledTierKeys)
}

func removeNoLongerBundledTiers(ctx context.Context, client runtimeclient.Client, namespace string, bundledTierKeys []runtimeclient.ObjectKey) error {
allTiers := &toolchainv1alpha1.NSTemplateTierList{}
if err := client.List(ctx, allTiers, runtimeclient.InNamespace(namespace)); err != nil {
return err
}

Check warning on line 60 in pkg/templates/nstemplatetiers/nstemplatetier_generator.go

View check run for this annotation

Codecov / codecov/patch

pkg/templates/nstemplatetiers/nstemplatetier_generator.go#L59-L60

Added lines #L59 - L60 were not covered by tests

var allErrors []error
for _, tier := range allTiers.Items {
if tier.Annotations[toolchainv1alpha1.BundledAnnotationKey] == constants.BundledWithHostOperatorAnnotationValue &&
!slices.Contains(bundledTierKeys, runtimeclient.ObjectKeyFromObject(&tier)) {
if err := client.Delete(ctx, &tier); err != nil {
allErrors = append(allErrors, err)

Check warning on line 67 in pkg/templates/nstemplatetiers/nstemplatetier_generator.go

View check run for this annotation

Codecov / codecov/patch

pkg/templates/nstemplatetiers/nstemplatetier_generator.go#L67

Added line #L67 was not covered by tests
}
return true, nil
}
applyCl := commonclient.NewApplyClient(client)
return applyCl.ApplyObject(ctx, toEnsure, commonclient.ForceUpdate(true))
}, namespace, metadata, files)
}

err := errors.Join(allErrors...)
if err != nil {
err = fmt.Errorf("failed to delete some of the no-longer-bundled NSTemplateTiers: %w", err)
}

Check warning on line 75 in pkg/templates/nstemplatetiers/nstemplatetier_generator.go

View check run for this annotation

Codecov / codecov/patch

pkg/templates/nstemplatetiers/nstemplatetier_generator.go#L74-L75

Added lines #L74 - L75 were not covered by tests

return err
}

// LoadFiles takes the file from deploy/nstemplatetiers/<tiername>/<yaml file name> . the folder structure should be 4 steps .
Expand All @@ -48,13 +83,13 @@
// load templates from assets
metadataContent, err := nsTemplateTiers.ReadFile(filepath.Join(root, "metadata.yaml"))
if err != nil {
return nil, nil, errors.Wrapf(err, "unable to load templates")
return nil, nil, fmt.Errorf("unable to load templates: %w", err)
}

metadata = make(map[string]string)
err = yaml.Unmarshal(metadataContent, &metadata)
if err != nil {
return nil, nil, errors.Wrapf(err, "unable to load templates")
return nil, nil, fmt.Errorf("unable to load templates: %w", err)

Check warning on line 92 in pkg/templates/nstemplatetiers/nstemplatetier_generator.go

View check run for this annotation

Codecov / codecov/patch

pkg/templates/nstemplatetiers/nstemplatetier_generator.go#L92

Added line #L92 was not covered by tests
}

paths, err := templates.GetAllFileNames(&nsTemplateTiers, root)
Expand All @@ -79,7 +114,7 @@
fileName := filepath.Join(parts[2], parts[3])
content, err := nsTemplateTiers.ReadFile(name)
if err != nil {
return nil, nil, errors.Wrapf(err, "unable to load templates")
return nil, nil, fmt.Errorf("unable to load templates: %w", err)

Check warning on line 117 in pkg/templates/nstemplatetiers/nstemplatetier_generator.go

View check run for this annotation

Codecov / codecov/patch

pkg/templates/nstemplatetiers/nstemplatetier_generator.go#L117

Added line #L117 was not covered by tests
}
files[fileName] = content
}
Expand Down
88 changes: 47 additions & 41 deletions pkg/templates/nstemplatetiers/nstemplatetier_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import (
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/host-operator/deploy"
"github.com/codeready-toolchain/host-operator/pkg/apis"
"github.com/codeready-toolchain/host-operator/pkg/constants"
"github.com/codeready-toolchain/host-operator/pkg/templates/nstemplatetiers"
tiertest "github.com/codeready-toolchain/host-operator/test/nstemplatetier"
commontest "github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -50,8 +52,7 @@ func roles(_ string) []string {
return []string{"admin"}
}

func TestCreateOrUpdateResourcesWitProdAssets(t *testing.T) {

func TestSyncResourcesWitProdAssets(t *testing.T) {
s := scheme.Scheme
err := apis.AddToScheme(s)
require.NoError(t, err)
Expand All @@ -60,7 +61,7 @@ func TestCreateOrUpdateResourcesWitProdAssets(t *testing.T) {
cl := commontest.NewFakeClient(t)

// when
err = nstemplatetiers.CreateOrUpdateResources(context.TODO(), s, cl, namespace)
err = nstemplatetiers.SyncResources(context.TODO(), s, cl, namespace)

// then
require.NoError(t, err)
Expand Down Expand Up @@ -109,72 +110,44 @@ func TestCreateOrUpdateResourcesWitProdAssets(t *testing.T) {
}

t.Run("failures", func(t *testing.T) {

namespace := "host-operator" + uuid.Must(uuid.NewV4()).String()[:7]
t.Run("nstemplatetiers", func(t *testing.T) {

t.Run("failed to create nstemplatetiers", func(t *testing.T) {
t.Run("failed to patch nstemplatetiers", func(t *testing.T) {
// given
clt := commontest.NewFakeClient(t)
clt.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error {
clt.MockPatch = func(ctx context.Context, obj runtimeclient.Object, patch runtimeclient.Patch, opts ...runtimeclient.PatchOption) error {
if obj.GetObjectKind().GroupVersionKind().Kind == "NSTemplateTier" && obj.GetName() == "base" {
// simulate a client/server error
return errors.Errorf("an error")
}
return clt.Client.Create(ctx, obj, opts...)
}
// when
err := nstemplatetiers.CreateOrUpdateResources(context.TODO(), s, clt, namespace)
// then
require.Error(t, err)
assert.Regexp(t, "unable to create NSTemplateTiers: unable to create or update the 'base' NSTemplateTier: unable to create resource of kind: NSTemplateTier, version: v1alpha1: an error", err.Error())
})

t.Run("failed to update nstemplatetiers", func(t *testing.T) {
// given
// initialize the client with an existing `advanced` NSTemplatetier
clt := commontest.NewFakeClient(t, &toolchainv1alpha1.NSTemplateTier{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "advanced",
},
})
clt.MockUpdate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error {
if obj.GetObjectKind().GroupVersionKind().Kind == "NSTemplateTier" && obj.GetName() == "advanced" {
// simulate a client/server error
return errors.Errorf("an error")
}
return clt.Client.Update(ctx, obj, opts...)
return commontest.Patch(ctx, clt, obj, patch, opts...)
}

// when
err := nstemplatetiers.CreateOrUpdateResources(context.TODO(), s, clt, namespace)
err := nstemplatetiers.SyncResources(context.TODO(), s, clt, namespace)
// then
require.Error(t, err)
assert.Contains(t, err.Error(), "unable to create NSTemplateTiers: unable to create or update the 'advanced' NSTemplateTier: unable to create resource of kind: NSTemplateTier, version: v1alpha1: unable to update the resource")
assert.Regexp(t, "unable to create NSTemplateTiers: unable to create or update the 'base' NSTemplateTier: unable to patch 'toolchain.dev.openshift.com/v1alpha1, Kind=NSTemplateTier' called 'base' in namespace '[a-zA-Z0-9-]+': an error", err.Error())
})
})

t.Run("tiertemplates", func(t *testing.T) {

t.Run("failed to create nstemplatetiers", func(t *testing.T) {
t.Run("failed to create tiertemplate", func(t *testing.T) {
// given
clt := commontest.NewFakeClient(t)
clt.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error {
clt.MockPatch = func(ctx context.Context, obj runtimeclient.Object, patch runtimeclient.Patch, opts ...runtimeclient.PatchOption) error {
if strings.HasPrefix(obj.GetName(), "base1ns-dev-") {
// simulate a client/server error
return errors.Errorf("an error")
}
return clt.Client.Create(ctx, obj, opts...)
return commontest.Patch(ctx, clt, obj, patch, opts...)
}
// when
err := nstemplatetiers.CreateOrUpdateResources(context.TODO(), s, clt, namespace)
err := nstemplatetiers.SyncResources(context.TODO(), s, clt, namespace)
// then
require.Error(t, err)
assert.Regexp(t, fmt.Sprintf("unable to create TierTemplates: unable to create the 'base1ns-dev-\\w+-\\w+' TierTemplate in namespace '%s'", namespace), err.Error()) // we can't tell for sure which namespace will fail first, but the error should match the given regex
})
})

})
t.Run("failed to load assets", func(t *testing.T) {
// when
Expand All @@ -183,7 +156,40 @@ func TestCreateOrUpdateResourcesWitProdAssets(t *testing.T) {
require.Error(t, err)
assert.Equal(t, "unable to load templates: open /templates/nstemplatetiers/metadata.yaml: file does not exist", err.Error()) // error occurred while creating TierTemplate resources
})
t.Run("tier that is no longer bundled is deleted", func(t *testing.T) {
// given
testTier := tiertest.TierInNamespace(t,
"not-bundled",
namespace,
toolchainv1alpha1.NSTemplateTierSpec{},
tiertest.MarkedBundled())

clt := commontest.NewFakeClient(t, testTier)

// when
err := nstemplatetiers.SyncResources(context.TODO(), clt.Scheme(), clt, namespace)
inCluster := &toolchainv1alpha1.NSTemplateTier{}
gerr := clt.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(testTier), inCluster)

// then
require.NoError(t, err)
require.True(t, apierrors.IsNotFound(gerr))
})
t.Run("bundled tiers are created with an annotation", func(t *testing.T) {
// given
clt := commontest.NewFakeClient(t)

// when
err := nstemplatetiers.SyncResources(context.TODO(), clt.Scheme(), clt, namespace)
inCluster := &toolchainv1alpha1.NSTemplateTier{}
// we know that the "base" tier is bundled
gerr := clt.Get(context.TODO(), runtimeclient.ObjectKey{Name: "base", Namespace: namespace}, inCluster)

// then
require.NoError(t, err)
require.NoError(t, gerr)
assert.Equal(t, constants.BundledWithHostOperatorAnnotationValue, inCluster.Annotations[toolchainv1alpha1.BundledAnnotationKey])
})
}

func verifyTierTemplate(t *testing.T, cl *commontest.FakeClient, namespace, tierName, typeName string) string {
Expand Down
18 changes: 17 additions & 1 deletion test/nstemplatetier/nstemplatetier.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"testing"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/host-operator/pkg/constants"
"github.com/codeready-toolchain/toolchain-common/pkg/hash"
"github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/codeready-toolchain/toolchain-common/pkg/test/nstemplateset"
Expand Down Expand Up @@ -176,9 +177,13 @@
}

func Tier(t *testing.T, name string, spec toolchainv1alpha1.NSTemplateTierSpec, options ...TierOption) *toolchainv1alpha1.NSTemplateTier {
return TierInNamespace(t, name, "toolchain-host-operator", spec, options...)

Check warning on line 180 in test/nstemplatetier/nstemplatetier.go

View check run for this annotation

Codecov / codecov/patch

test/nstemplatetier/nstemplatetier.go#L180

Added line #L180 was not covered by tests
}

func TierInNamespace(t *testing.T, name, namespace string, spec toolchainv1alpha1.NSTemplateTierSpec, options ...TierOption) *toolchainv1alpha1.NSTemplateTier {

Check warning on line 183 in test/nstemplatetier/nstemplatetier.go

View check run for this annotation

Codecov / codecov/patch

test/nstemplatetier/nstemplatetier.go#L183

Added line #L183 was not covered by tests
tier := &toolchainv1alpha1.NSTemplateTier{
ObjectMeta: metav1.ObjectMeta{
Namespace: "toolchain-host-operator",
Namespace: namespace,

Check warning on line 186 in test/nstemplatetier/nstemplatetier.go

View check run for this annotation

Codecov / codecov/patch

test/nstemplatetier/nstemplatetier.go#L186

Added line #L186 was not covered by tests
Name: name,
},
Spec: spec,
Expand Down Expand Up @@ -246,12 +251,23 @@
}
}

// WithFinalizer adds the finalizer to the tier
func WithFinalizer() TierOption {
return func(nt *toolchainv1alpha1.NSTemplateTier) {
controllerutil.AddFinalizer(nt, toolchainv1alpha1.FinalizerName)
}
}

// MarkedBundled marks the tier as bundled by adding the appropriate annotation
func MarkedBundled() TierOption {
return func(tier *toolchainv1alpha1.NSTemplateTier) {
if tier.Annotations == nil {
tier.Annotations = map[string]string{}
}
tier.Annotations[toolchainv1alpha1.BundledAnnotationKey] = constants.BundledWithHostOperatorAnnotationValue

Check warning on line 267 in test/nstemplatetier/nstemplatetier.go

View check run for this annotation

Codecov / codecov/patch

test/nstemplatetier/nstemplatetier.go#L262-L267

Added lines #L262 - L267 were not covered by tests
}
}

// OtherTier returns an "other" NSTemplateTier
func OtherTier(t *testing.T, options ...TierOption) *toolchainv1alpha1.NSTemplateTier {
return Tier(t, "other", toolchainv1alpha1.NSTemplateTierSpec{
Expand Down
Loading