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/catalogmetadata/filter/bundle_predicates_test.go b/internal/catalogmetadata/filter/bundle_predicates_test.go index 3617f47ce..f64bca6c7 100644 --- a/internal/catalogmetadata/filter/bundle_predicates_test.go +++ b/internal/catalogmetadata/filter/bundle_predicates_test.go @@ -62,6 +62,53 @@ func TestInMastermindsSemverRange(t *testing.T) { assert.False(t, f(b3)) } +func TestHigherBundleVersion(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0"}`), + }, + }, + }} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "0.0.1"}`), + }, + }, + }} + b3 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "2.0.0"}`), + }, + }, + }} + b4 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "broken"}`), + }, + }, + }} + var nilVersion *bsemver.Version + version := bsemver.MustParse("1.0.0") + + f := filter.HigherBundleVersion(&version) + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.True(t, f(b3)) + assert.False(t, f(b4)) + + nilFilter := filter.HigherBundleVersion(nilVersion) + assert.False(t, nilFilter(b1)) +} + func TestInBlangSemverRange(t *testing.T) { b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ Properties: []property.Property{ diff --git a/internal/controllers/extension_controller.go b/internal/controllers/extension_controller.go index e15bc3a72..a49de5456 100644 --- a/internal/controllers/extension_controller.go +++ b/internal/controllers/extension_controller.go @@ -18,7 +18,6 @@ package controllers import ( "context" - "errors" "fmt" "sort" "strings" @@ -31,6 +30,7 @@ import ( 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 +55,11 @@ import ( type ExtensionReconciler struct { client.Client BundleProvider BundleProvider - HasKappApis bool } -var errkappAPIUnavailable = errors.New("kapp-controller apis unavailable on cluster") +var ( + bundleVersionKey = "olm.operatorframework.io/bundleVersion" +) //+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 +146,6 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext return ctrl.Result{}, nil } - if !r.HasKappApis { - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, errkappAPIUnavailable.Error(), ext.GetGeneration()) - - ext.Status.ResolvedBundle = nil - 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 +179,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.InstalledBundle = nil @@ -402,7 +397,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 @@ -436,6 +431,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, }, @@ -454,6 +452,23 @@ func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle return app } +func (r *ExtensionReconciler) getInstalledVersion(ctx context.Context, extNN types.NamespacedName) (*bsemver.Version, error) { + existingApp, err := r.existingAppUnstructured(ctx, extNN.Name, extNN.Namespace) + if err != nil { + return nil, err + } + existingVersion, ok := existingApp.GetAnnotations()[bundleVersionKey] + if !ok { + return nil, fmt.Errorf("existing App %q in Namespace %q missing bundle version", extNN.Name, extNN.Namespace) + } + + existingVersionSemver, err := bsemver.New(existingVersion) + if err != nil { + return nil, fmt.Errorf("could not determine bundle version of existing App %q in Namespace %q: %w", extNN.Name, extNN.Namespace, err) + } + return existingVersionSemver, nil +} + func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1.Extension) (*catalogmetadata.Bundle, error) { allBundles, err := r.BundleProvider.Bundles(ctx) if err != nil { @@ -480,19 +495,33 @@ func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1. predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) } + var installedVersion string + // Do not include bundle versions older than currently installed unless UpgradeConstraintPolicy = 'Ignore' + if extension.Spec.Source.Package.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore { + installedVersionSemver, err := r.getInstalledVersion(ctx, types.NamespacedName{Name: extension.GetName(), Namespace: extension.GetNamespace()}) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + if installedVersionSemver != nil { + installedVersion = installedVersionSemver.String() + predicates = append(predicates, catalogfilter.HigherBundleVersion(installedVersionSemver)) + } + } + 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 24ddb2fc9..10839a063 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.operatorframework.io/bundleVersion": "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.operatorframework.io/bundleVersion": "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.operatorframework.io/bundleVersion": "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: {}