From 5e80cd544919ca40c253e5fbe656747372c0a4ce Mon Sep 17 00:00:00 2001 From: dtfranz Date: Fri, 22 Mar 2024 10:46:45 -0400 Subject: [PATCH] Filter out bundle versions lower than installed Adds an annotation to the App created for each extension indicating the bundle version, which can then be used on future reconciles to filter out older versions. This can be overridden by setting the UpgradeConstraintPolicy to 'Ignore'. Co-authored-by: Varsha Prasad Narsing Signed-off-by: dtfranz --- cmd/manager/main.go | 35 - .../filter/bundle_predicates.go | 13 + internal/controllers/extension_controller.go | 62 +- .../controllers/extension_controller_test.go | 152 ++++ internal/controllers/suite_test.go | 6 +- testdata/crds/kappctrl.k14s.io_app.yaml | 728 ++++++++++++++++++ 6 files changed, 938 insertions(+), 58 deletions(-) create mode 100644 testdata/crds/kappctrl.k14s.io_app.yaml diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9aab1dd41..5e887ed20 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -18,7 +18,6 @@ package main import ( "flag" - "fmt" "net/http" "os" "time" @@ -28,13 +27,10 @@ import ( rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" "github.com/spf13/pflag" "go.uber.org/zap/zapcore" - "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/discovery" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -131,16 +127,9 @@ func main() { os.Exit(1) } - hasKappApis, err := hasKappApis(mgr.GetConfig()) - if err != nil { - setupLog.Error(err, "unable to evaluate if App needs to be created") - os.Exit(1) - } - if err = (&controllers.ExtensionReconciler{ Client: cl, BundleProvider: catalogClient, - HasKappApis: hasKappApis, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Extension") os.Exit(1) @@ -162,27 +151,3 @@ func main() { os.Exit(1) } } - -// hasKappApis checks whether the cluster has Kapp APIs installed in the cluster. -// This does not guarantee that the controller is present to reconcile the App CRs. -func hasKappApis(config *rest.Config) (bool, error) { - discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) - if err != nil { - return false, fmt.Errorf("creating discovery client: %v", err) - } - apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(carvelv1alpha1.SchemeGroupVersion.String()) - if err != nil && !errors.IsNotFound(err) { - return false, fmt.Errorf("listing resource APIs: %v", err) - } - - if apiResourceList == nil { - return false, nil - } - - for _, resource := range apiResourceList.APIResources { - if resource.Kind == "App" { - return true, nil - } - } - return false, nil -} diff --git a/internal/catalogmetadata/filter/bundle_predicates.go b/internal/catalogmetadata/filter/bundle_predicates.go index 1d5761a95..7419aeae0 100644 --- a/internal/catalogmetadata/filter/bundle_predicates.go +++ b/internal/catalogmetadata/filter/bundle_predicates.go @@ -31,6 +31,19 @@ func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[catal } } +func HigherBundleVersion(currentVersion *bsemver.Version) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { + if currentVersion == nil { + return false + } + bundleVersion, err := bundle.Version() + if err != nil { + return false + } + return bundleVersion.GTE(*currentVersion) + } +} + func InBlangSemverRange(semverRange bsemver.Range) Predicate[catalogmetadata.Bundle] { return func(bundle *catalogmetadata.Bundle) bool { bundleVersion, err := bundle.Version() diff --git a/internal/controllers/extension_controller.go b/internal/controllers/extension_controller.go index 04310164c..5725e7ca4 100644 --- a/internal/controllers/extension_controller.go +++ b/internal/controllers/extension_controller.go @@ -18,18 +18,19 @@ package controllers import ( "context" - "errors" "fmt" "sort" "strings" mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" "github.com/go-logr/logr" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" "github.com/operator-framework/operator-registry/alpha/declcfg" kappctrlv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -55,10 +56,11 @@ import ( type ExtensionReconciler struct { client.Client BundleProvider BundleProvider - HasKappApis bool } -var errkappAPIUnavailable = errors.New("kapp-controller apis unavailable on cluster") +var ( + bundleVersionKey = "olm.bundle-version" +) //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/status,verbs=update;patch @@ -145,17 +147,6 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext return ctrl.Result{}, nil } - if !r.HasKappApis { - ext.Status.InstalledBundleResource = "" - setInstalledStatusConditionFailed(&ext.Status.Conditions, errkappAPIUnavailable.Error(), ext.GetGeneration()) - - ext.Status.ResolvedBundleResource = "" - setResolvedStatusConditionUnknown(&ext.Status.Conditions, "kapp apis are unavailable", ext.GetGeneration()) - - setDeprecationStatusesUnknown(&ext.Status.Conditions, "kapp apis are unavailable", ext.GetGeneration()) - return ctrl.Result{}, errkappAPIUnavailable - } - // TODO: Improve the resolution logic. bundle, err := r.resolve(ctx, *ext) if err != nil { @@ -189,7 +180,12 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext return ctrl.Result{}, nil } - app := r.GenerateExpectedApp(*ext, bundle.Image) + bundleVersion, err := bundle.Version() + if err != nil { + l.Info("could not determine bundle version", "name", ext.GetName(), "namespace", ext.GetNamespace()) + } + + app := r.GenerateExpectedApp(*ext, bundle.Image, bundleVersion.String()) if err := r.ensureApp(ctx, app); err != nil { // originally Reason: ocv1alpha1.ReasonInstallationFailed ext.Status.InstalledBundleResource = "" @@ -404,7 +400,7 @@ func extensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.Ma } } -func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundlePath string) *unstructured.Unstructured { +func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundlePath string, bundleVersion string) *unstructured.Unstructured { // We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver. // If you use a typed object, any default values from that struct get serialized into the JSON patch, which could // cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an @@ -438,6 +434,9 @@ func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle "metadata": map[string]interface{}{ "name": o.GetName(), "namespace": o.GetNamespace(), + "annotations": map[string]string{ + bundleVersionKey: bundleVersion, + }, }, "spec": spec, }, @@ -482,19 +481,38 @@ func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1. predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) } + // Do not include versions older than currently installed unless UpgradeConstraintPolicy = 'Ignore' + var installedVersion string + if extension.Spec.Source.Package.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore { + existingApp, err := r.existingAppUnstructured(ctx, extension.GetName(), extension.GetNamespace()) + if err != nil { + // No need for an error if the App hasn't been created yet + if !apierrors.IsNotFound(err) { + return nil, err + } + } else if existingVersion, ok := existingApp.GetAnnotations()[bundleVersionKey]; ok { + existingVersionSemver, err := bsemver.New(existingVersion) + if err == nil { + installedVersion = existingVersion + predicates = append(predicates, catalogfilter.HigherBundleVersion(existingVersionSemver)) + } + } + } + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) if len(resultSet) == 0 { - if versionRange != "" && channelName != "" { - return nil, fmt.Errorf("no package %q matching version %q found in channel %q", packageName, versionRange, channelName) - } + var versionError, channelError, existingVersionError string if versionRange != "" { - return nil, fmt.Errorf("no package %q matching version %q found", packageName, versionRange) + versionError = fmt.Sprintf(" matching version %q", versionRange) } if channelName != "" { - return nil, fmt.Errorf("no package %q found in channel %q", packageName, channelName) + channelError = fmt.Sprintf(" in channel %q", channelName) + } + if installedVersion != "" { + existingVersionError = fmt.Sprintf(" which upgrades currently installed version %q", installedVersion) } - return nil, fmt.Errorf("no package %q found", packageName) + return nil, fmt.Errorf("no package %q%s%s%s found", packageName, versionError, channelError, existingVersionError) } sort.SliceStable(resultSet, func(i, j int) bool { diff --git a/internal/controllers/extension_controller_test.go b/internal/controllers/extension_controller_test.go index 0d59995a9..0140401d4 100644 --- a/internal/controllers/extension_controller_test.go +++ b/internal/controllers/extension_controller_test.go @@ -7,18 +7,24 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" featuregatetesting "k8s.io/component-base/featuregate/testing" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/conditionsets" "github.com/operator-framework/operator-controller/pkg/features" ) +const ( + testServiceAccount = "test-sa" +) + // Describe: Extension Controller Test func TestExtensionDoesNotExist(t *testing.T) { _, reconciler := newClientAndExtensionReconciler(t) @@ -92,6 +98,152 @@ func TestExtensionReconcile(t *testing.T) { } } +func TestExtensionResolve(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.EnableExtensionAPI, true)() + ctx := context.Background() + + testCases := []struct { + name string + packageName string + packageVersion string + upgradeConstraintPolicy ocv1alpha1.UpgradeConstraintPolicy + wantErr error + existingApp *carvelv1alpha1.App + assert func(*testing.T, ctrl.Result, client.Client, types.NamespacedName) + }{ + { + name: "basic install with specified version", + packageName: "prometheus", + packageVersion: "0.37.0", + assert: func(t *testing.T, res ctrl.Result, c client.Client, extNN types.NamespacedName) { + ext := &ocv1alpha1.Extension{} + err := c.Get(ctx, extNN, ext) + assert.NoError(t, err) + + assert.Equal(t, ctrl.Result{}, res) + + condition := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeResolved) + assert.NotNil(t, condition) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, condition.Message) + }, + }, + { + name: "existing App of same version", + packageName: "prometheus", + packageVersion: "0.37.0", + existingApp: &carvelv1alpha1.App{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "olm.bundle-version": "0.37.0", + }, + }, + Spec: carvelv1alpha1.AppSpec{}, + }, + assert: func(t *testing.T, res ctrl.Result, c client.Client, extNN types.NamespacedName) { + ext := &ocv1alpha1.Extension{} + err := c.Get(ctx, extNN, ext) + assert.NoError(t, err) + + assert.Equal(t, ctrl.Result{}, res) + + condition := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeResolved) + assert.NotNil(t, condition) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, condition.Message) + }, + }, + { + name: "existing App of higher version than available", + packageName: "prometheus", + packageVersion: "0.37.0", + existingApp: &carvelv1alpha1.App{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "olm.bundle-version": "0.38.0", + }, + }, + Spec: carvelv1alpha1.AppSpec{}, + }, + wantErr: fmt.Errorf("no package \"prometheus\" matching version \"0.37.0\" which upgrades currently installed version \"0.38.0\" found"), + assert: func(t *testing.T, res ctrl.Result, c client.Client, extNN types.NamespacedName) { + ext := &ocv1alpha1.Extension{} + err := c.Get(ctx, extNN, ext) + assert.NoError(t, err) + + assert.Equal(t, ctrl.Result{}, res) + + condition := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeResolved) + assert.NotNil(t, condition) + assert.Equal(t, metav1.ConditionFalse, condition.Status) + assert.Equal(t, `no package "prometheus" matching version "0.37.0" which upgrades currently installed version "0.38.0" found`, condition.Message) + }, + }, + { + name: "downgrade with UpgradeConstraintPolicy of 'Ignore'", + packageName: "prometheus", + packageVersion: "0.37.0", + upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, + existingApp: &carvelv1alpha1.App{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "olm.bundle-version": "0.38.0", + }, + }, + Spec: carvelv1alpha1.AppSpec{}, + }, + assert: func(t *testing.T, res ctrl.Result, c client.Client, extNN types.NamespacedName) { + ext := &ocv1alpha1.Extension{} + err := c.Get(ctx, extNN, ext) + assert.NoError(t, err) + + assert.Equal(t, ctrl.Result{}, res) + + condition := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeResolved) + assert.NotNil(t, condition) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, condition.Message) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c, reconciler := newClientAndExtensionReconciler(t) + extName := fmt.Sprintf("extension-test-%s", rand.String(8)) + ext := &ocv1alpha1.Extension{ + ObjectMeta: metav1.ObjectMeta{Name: extName, Namespace: "default"}, + Spec: ocv1alpha1.ExtensionSpec{ + ServiceAccountName: testServiceAccount, + Source: ocv1alpha1.ExtensionSource{ + SourceType: ocv1alpha1.SourceTypePackage, + Package: &ocv1alpha1.ExtensionSourcePackage{ + Name: tc.packageName, + Version: tc.packageVersion, + UpgradeConstraintPolicy: tc.upgradeConstraintPolicy, + }, + }, + }, + } + if tc.existingApp != nil { + tc.existingApp.Name = extName + require.NoError(t, c.Create(ctx, tc.existingApp)) + } + + require.NoError(t, c.Create(ctx, ext)) + extNN := types.NamespacedName{Name: ext.GetName(), Namespace: ext.GetNamespace()} + + res, reconcileErr := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extNN}) + assert.Equal(t, reconcileErr, tc.wantErr) + + tc.assert(t, res, c, extNN) + }) + } +} + func verifyExtensionInvariants(t *testing.T, ext *ocv1alpha1.Extension) { verifyExtensionConditionsInvariants(t, ext) } diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index 8ffcd31e5..d208e826a 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -25,6 +25,7 @@ import ( "github.com/operator-framework/deppy/pkg/deppy/solver" rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" "github.com/stretchr/testify/require" + carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -61,8 +62,10 @@ func newClientAndReconciler(t *testing.T) (client.Client, *controllers.ClusterEx func newClientAndExtensionReconciler(t *testing.T) (client.Client, *controllers.ExtensionReconciler) { cl := newClient(t) + fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList) reconciler := &controllers.ExtensionReconciler{ - Client: cl, + Client: cl, + BundleProvider: &fakeCatalogClient, } return cl, reconciler } @@ -91,6 +94,7 @@ func TestMain(m *testing.M) { utilruntime.Must(ocv1alpha1.AddToScheme(sch)) utilruntime.Must(rukpakv1alpha2.AddToScheme(sch)) utilruntime.Must(corev1.AddToScheme(sch)) + utilruntime.Must(carvelv1alpha1.AddToScheme(sch)) code := m.Run() utilruntime.Must(testEnv.Stop()) diff --git a/testdata/crds/kappctrl.k14s.io_app.yaml b/testdata/crds/kappctrl.k14s.io_app.yaml new file mode 100644 index 000000000..a3b3f987d --- /dev/null +++ b/testdata/crds/kappctrl.k14s.io_app.yaml @@ -0,0 +1,728 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: apps.kappctrl.k14s.io +spec: + group: kappctrl.k14s.io + names: + categories: + - carvel + kind: App + listKind: AppList + plural: apps + singular: app + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Friendly description + jsonPath: .status.friendlyDescription + name: Description + type: string + - description: Last time app started being deployed. Does not mean anything was changed. + jsonPath: .status.deploy.startedAt + name: Since-Deploy + type: date + - description: Time since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'An App is a set of Kubernetes resources. These resources could span any number of namespaces or could be cluster-wide (e.g. CRDs). An App is represented in kapp-controller using a App CR. The App CR comprises of three main sections: spec.fetch – declare source for fetching configuration and OCI images spec.template – declare templating tool and values spec.deploy – declare deployment tool and any deploy specific configuration' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + canceled: + description: Cancels current and future reconciliations (optional; default=false) + type: boolean + cluster: + description: Specifies that app should be deployed to destination cluster; by default, cluster is same as where this resource resides (optional; v0.5.0+) + properties: + kubeconfigSecretRef: + description: Specifies secret containing kubeconfig (required) + properties: + key: + description: Specifies key that contains kubeconfig (optional) + type: string + name: + description: Specifies secret name within app's namespace (required) + type: string + type: object + namespace: + description: Specifies namespace in destination cluster (optional) + type: string + type: object + defaultNamespace: + description: Specifies the default namespace to install the App resources, by default this is same as the App's namespace (optional; v0.48.0+) + type: string + deploy: + items: + properties: + kapp: + description: Use kapp to deploy resources + properties: + delete: + description: Configuration for delete command (optional) + properties: + rawOptions: + description: Pass through options to kapp delete (optional) + items: + type: string + type: array + type: object + inspect: + description: 'Configuration for inspect command (optional) as of kapp-controller v0.31.0, inspect is disabled by default add rawOptions or use an empty inspect config like `inspect: {}` to enable' + properties: + rawOptions: + description: Pass through options to kapp inspect (optional) + items: + type: string + type: array + type: object + intoNs: + description: Override namespace for all resources (optional) + type: string + mapNs: + description: Provide custom namespace override mapping (optional) + items: + type: string + type: array + rawOptions: + description: Pass through options to kapp deploy (optional) + items: + type: string + type: array + type: object + type: object + type: array + fetch: + items: + properties: + git: + description: Uses git to clone repository + properties: + forceHTTPBasicAuth: + description: Force the usage of HTTP Basic Auth when Basic Auth is provided (optional) + type: boolean + lfsSkipSmudge: + description: Skip lfs download (optional) + type: boolean + ref: + description: Branch, tag, commit; origin is the name of the remote (optional) + type: string + refSelection: + description: Specifies a strategy to resolve to an explicit ref (optional; v0.24.0+) + properties: + semver: + properties: + constraints: + type: string + prereleases: + properties: + identifiers: + items: + type: string + type: array + type: object + type: object + type: object + secretRef: + description: 'Secret with auth details. allowed keys: ssh-privatekey, ssh-knownhosts, username, password (optional) (if ssh-knownhosts is not specified, git will not perform strict host checking)' + properties: + name: + description: Object is expected to be within same namespace + type: string + type: object + subPath: + description: Grab only portion of repository (optional) + type: string + url: + description: http or ssh urls are supported (required) + type: string + type: object + helmChart: + description: Uses helm fetch to fetch specified chart + properties: + name: + description: 'Example: stable/redis' + type: string + repository: + properties: + secretRef: + properties: + name: + description: Object is expected to be within same namespace + type: string + type: object + url: + description: Repository url; scheme of oci:// will fetch experimental helm oci chart (v0.19.0+) (required) + type: string + type: object + version: + type: string + type: object + http: + description: Uses http library to fetch file + properties: + secretRef: + description: 'Secret to provide auth details (optional) Secret may include one or more keys: username, password' + properties: + name: + description: Object is expected to be within same namespace + type: string + type: object + sha256: + description: Checksum to verify after download (optional) + type: string + subPath: + description: Grab only portion of download (optional) + type: string + url: + description: 'URL can point to one of following formats: text, tgz, zip http and https url are supported; plain file, tgz and tar types are supported (required)' + type: string + type: object + image: + description: Pulls content from Docker/OCI registry + properties: + secretRef: + description: 'Secret may include one or more keys: username, password, token. By default anonymous access is used for authentication.' + properties: + name: + description: Object is expected to be within same namespace + type: string + type: object + subPath: + description: Grab only portion of image (optional) + type: string + tagSelection: + description: Specifies a strategy to choose a tag (optional; v0.24.0+) if specified, do not include a tag in url key + properties: + semver: + properties: + constraints: + type: string + prereleases: + properties: + identifiers: + items: + type: string + type: array + type: object + type: object + type: object + url: + description: 'Docker image url; unqualified, tagged, or digest references supported (required) Example: username/app1-config:v0.1.0' + type: string + type: object + imgpkgBundle: + description: Pulls imgpkg bundle from Docker/OCI registry (v0.17.0+) + properties: + image: + description: Docker image url; unqualified, tagged, or digest references supported (required) + type: string + secretRef: + description: 'Secret may include one or more keys: username, password, token. By default anonymous access is used for authentication.' + properties: + name: + description: Object is expected to be within same namespace + type: string + type: object + tagSelection: + description: Specifies a strategy to choose a tag (optional; v0.24.0+) if specified, do not include a tag in url key + properties: + semver: + properties: + constraints: + type: string + prereleases: + properties: + identifiers: + items: + type: string + type: array + type: object + type: object + type: object + type: object + inline: + description: Pulls content from within this resource; or other resources in the cluster + properties: + paths: + additionalProperties: + type: string + description: Specifies mapping of paths to their content; not recommended for sensitive values as CR is not encrypted (optional) + type: object + pathsFrom: + description: Specifies content via secrets and config maps; data values are recommended to be placed in secrets (optional) + items: + properties: + configMapRef: + properties: + directoryPath: + description: Specifies where to place files found in secret (optional) + type: string + name: + type: string + type: object + secretRef: + properties: + directoryPath: + description: Specifies where to place files found in secret (optional) + type: string + name: + type: string + type: object + type: object + type: array + type: object + path: + description: Relative path to place the fetched artifacts + type: string + type: object + type: array + noopDelete: + description: Deletion requests for the App will result in the App CR being deleted, but its associated resources will not be deleted (optional; default=false; v0.18.0+) + type: boolean + paused: + description: Pauses _future_ reconciliation; does _not_ affect currently running reconciliation (optional; default=false) + type: boolean + serviceAccountName: + description: Specifies that app should be deployed authenticated via given service account, found in this namespace (optional; v0.6.0+) + type: string + syncPeriod: + description: Specifies the length of time to wait, in time + unit format, before reconciling. Always >= 30s. If value below 30s is specified, 30s will be used. (optional; v0.9.0+; default=30s) + type: string + template: + items: + properties: + cue: + properties: + inputExpression: + description: Cue expression for single path component, can be used to unify ValuesFrom into a given field (optional) + type: string + outputExpression: + description: Cue expression to output, default will export all visible fields (optional) + type: string + paths: + description: Explicit list of files/directories (optional) + items: + type: string + type: array + valuesFrom: + description: Provide values (optional) + items: + properties: + configMapRef: + properties: + name: + type: string + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldPath: + description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' + type: string + kappControllerVersion: + description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' + properties: + version: + type: string + type: object + kubernetesAPIs: + description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' + properties: + groupVersions: + items: + type: string + type: array + type: object + kubernetesVersion: + description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' + properties: + version: + type: string + type: object + name: + type: string + type: object + type: array + type: object + path: + type: string + secretRef: + properties: + name: + type: string + type: object + type: object + type: array + type: object + helmTemplate: + description: Use helm template command to render helm chart + properties: + kubernetesAPIs: + description: 'Optional: Use kubernetes group/versions resources available in the live cluster' + properties: + groupVersions: + items: + type: string + type: array + type: object + kubernetesVersion: + description: 'Optional: Get Kubernetes version, defaults (empty) to retrieving the version from the cluster. Can be manually overridden to a value instead.' + properties: + version: + type: string + type: object + name: + description: Set name explicitly, default is App CR's name (optional; v0.13.0+) + type: string + namespace: + description: Set namespace explicitly, default is App CR's namespace (optional; v0.13.0+) + type: string + path: + description: Path to chart (optional; v0.13.0+) + type: string + valuesFrom: + description: One or more secrets, config maps, paths that provide values (optional) + items: + properties: + configMapRef: + properties: + name: + type: string + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldPath: + description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' + type: string + kappControllerVersion: + description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' + properties: + version: + type: string + type: object + kubernetesAPIs: + description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' + properties: + groupVersions: + items: + type: string + type: array + type: object + kubernetesVersion: + description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' + properties: + version: + type: string + type: object + name: + type: string + type: object + type: array + type: object + path: + type: string + secretRef: + properties: + name: + type: string + type: object + type: object + type: array + type: object + jsonnet: + description: TODO implement jsonnet + type: object + kbld: + description: Use kbld to resolve image references to use digests + properties: + paths: + items: + type: string + type: array + type: object + kustomize: + description: TODO implement kustomize + type: object + sops: + description: Use sops to decrypt *.sops.yml files (optional; v0.11.0+) + properties: + age: + properties: + privateKeysSecretRef: + description: Secret with private armored PGP private keys (required) + properties: + name: + type: string + type: object + type: object + paths: + description: Lists paths to decrypt explicitly (optional; v0.13.0+) + items: + type: string + type: array + pgp: + description: Use PGP to decrypt files (required) + properties: + privateKeysSecretRef: + description: Secret with private armored PGP private keys (required) + properties: + name: + type: string + type: object + type: object + type: object + ytt: + description: Use ytt to template configuration + properties: + fileMarks: + description: Control metadata about input files passed to ytt (optional; v0.18.0+) see https://carvel.dev/ytt/docs/latest/file-marks/ for more details + items: + type: string + type: array + ignoreUnknownComments: + description: Ignores comments that ytt doesn't recognize (optional; default=false) + type: boolean + inline: + description: Specify additional files, including data values (optional) + properties: + paths: + additionalProperties: + type: string + description: Specifies mapping of paths to their content; not recommended for sensitive values as CR is not encrypted (optional) + type: object + pathsFrom: + description: Specifies content via secrets and config maps; data values are recommended to be placed in secrets (optional) + items: + properties: + configMapRef: + properties: + directoryPath: + description: Specifies where to place files found in secret (optional) + type: string + name: + type: string + type: object + secretRef: + properties: + directoryPath: + description: Specifies where to place files found in secret (optional) + type: string + name: + type: string + type: object + type: object + type: array + type: object + paths: + description: Lists paths to provide to ytt explicitly (optional) + items: + type: string + type: array + strict: + description: Forces strict mode https://github.com/k14s/ytt/blob/develop/docs/strict.md (optional; default=false) + type: boolean + valuesFrom: + description: Provide values via ytt's --data-values-file (optional; v0.19.0-alpha.9) + items: + properties: + configMapRef: + properties: + name: + type: string + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldPath: + description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' + type: string + kappControllerVersion: + description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' + properties: + version: + type: string + type: object + kubernetesAPIs: + description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' + properties: + groupVersions: + items: + type: string + type: array + type: object + kubernetesVersion: + description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' + properties: + version: + type: string + type: object + name: + type: string + type: object + type: array + type: object + path: + type: string + secretRef: + properties: + name: + type: string + type: object + type: object + type: array + type: object + type: object + type: array + type: object + status: + properties: + conditions: + items: + properties: + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, this should be a short, machine understandable string that gives the reason for condition's last transition. If it reports "ResizeStarted" that means the underlying persistent volume is being resized. + type: string + status: + type: string + type: + description: ConditionType represents reconciler state + type: string + required: + - status + - type + type: object + type: array + consecutiveReconcileFailures: + type: integer + consecutiveReconcileSuccesses: + type: integer + deploy: + properties: + error: + type: string + exitCode: + type: integer + finished: + type: boolean + kapp: + description: KappDeployStatus contains the associated AppCR deployed resources + properties: + associatedResources: + description: AssociatedResources contains the associated App label, namespaces and GKs + properties: + groupKinds: + items: + description: GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types + properties: + group: + type: string + kind: + type: string + required: + - group + - kind + type: object + type: array + label: + type: string + namespaces: + items: + type: string + type: array + type: object + type: object + startedAt: + format: date-time + type: string + stderr: + type: string + stdout: + type: string + updatedAt: + format: date-time + type: string + type: object + fetch: + properties: + error: + type: string + exitCode: + type: integer + startedAt: + format: date-time + type: string + stderr: + type: string + stdout: + type: string + updatedAt: + format: date-time + type: string + type: object + friendlyDescription: + type: string + inspect: + properties: + error: + type: string + exitCode: + type: integer + stderr: + type: string + stdout: + type: string + updatedAt: + format: date-time + type: string + type: object + managedAppName: + type: string + observedGeneration: + description: Populated based on metadata.generation when controller observes a change to the resource; if this value is out of data, other status fields do not reflect latest state + format: int64 + type: integer + template: + properties: + error: + type: string + exitCode: + type: integer + stderr: + type: string + updatedAt: + format: date-time + type: string + type: object + usefulErrorMessage: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {}