diff --git a/cmd/manager/main.go b/cmd/manager/main.go index f1609ce10..325a17d0c 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -18,6 +18,7 @@ package main import ( "flag" + "fmt" "net/http" "os" "time" @@ -29,8 +30,10 @@ import ( "go.uber.org/zap/zapcore" "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" @@ -41,6 +44,7 @@ import ( catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/pkg/features" + carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" ) var ( @@ -54,6 +58,7 @@ func init() { utilruntime.Must(ocv1alpha1.AddToScheme(scheme)) utilruntime.Must(rukpakv1alpha2.AddToScheme(scheme)) utilruntime.Must(catalogd.AddToScheme(scheme)) + utilruntime.Must(carvelv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -124,8 +129,18 @@ func main() { os.Exit(1) } + hasKappApis, err := HasKappApis(mgr.GetConfig()) + // We should probably prefer to proceed with creating BD instead of exiting? + if err != nil { + setupLog.Error(err, "unable to evaluate if App needs to be created") + os.Exit(1) + } + if err = (&controllers.ExtensionReconciler{ - Client: cl, + Client: cl, + BundleProvider: catalogClient, + Resolver: resolver, + HasKappApis: hasKappApis, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Extension") os.Exit(1) @@ -147,3 +162,23 @@ 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 { + return false, fmt.Errorf("listing resource APIs: %v", err) + } + + for _, resource := range apiResourceList.APIResources { + if resource.Kind == "App" { + return true, nil + } + } + return false, nil +} \ No newline at end of file diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 91556a364..4094f1c68 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -29,6 +29,17 @@ rules: - patch - update - watch +- apiGroups: + - kappctrl.k14s.io + resources: + - apps + verbs: + - create + - get + - list + - patch + - update + - watch - apiGroups: - olm.operatorframework.io resources: @@ -73,5 +84,6 @@ rules: resources: - extensions/status verbs: + - get - patch - update diff --git a/go.mod b/go.mod index f1861f24a..1ab810adb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/operator-framework/operator-controller -go 1.20 +go 1.21 + +toolchain go1.21.0 require ( github.com/Masterminds/semver/v3 v3.2.1 @@ -13,6 +15,7 @@ require ( github.com/operator-framework/rukpak v0.18.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + github.com/vmware-tanzu/carvel-kapp-controller v0.50.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d gopkg.in/yaml.v2 v2.4.0 @@ -117,6 +120,7 @@ require ( github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/vbatts/tar-split v0.11.5 // indirect + github.com/vmware-tanzu/carvel-vendir v0.36.0 // indirect go.etcd.io/bbolt v1.3.8 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect diff --git a/go.sum b/go.sum index e7088980f..4bc282505 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -14,6 +15,7 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5 github.com/Microsoft/hcsshim v0.12.0-rc.1 h1:Hy+xzYujv7urO5wrgcG58SPMOXNLrj4WCJbySs2XX/A= github.com/Microsoft/hcsshim v0.12.0-rc.1/go.mod h1:Y1a1S0QlYp1mBpyvGiuEdOfZqnao+0uX5AWHXQ5NhZU= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= @@ -26,8 +28,11 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bugsnag/bugsnag-go v1.5.3 h1:yeRUT3mUE13jL1tGwvoQsKdVbAsQx9AJ+fqahKveP04= +github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -58,7 +63,9 @@ github.com/containers/ocicrypt v1.1.9/go.mod h1:dTKx1918d8TDkxXvarscpNVY+lyPakPN github.com/containers/storage v1.51.0 h1:AowbcpiWXzAjHosKz7MKvPEqpyX+ryZA/ZurytRrFNA= github.com/containers/storage v1.51.0/go.mod h1:ybl8a3j1PPtpyaEi/5A6TOFs+5TrEyObeKJzVtkUlfc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -77,9 +84,11 @@ github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHO github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= @@ -97,6 +106,7 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= +github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-air/gini v1.0.4 h1:lteMAxHKNOAjIqazL/klOJJmxq6YxxSuJ17MnMXny+s= github.com/go-air/gini v1.0.4/go.mod h1:dd8RvT1xcv6N1da33okvBd8DhMh1/A4siGy6ErjTljs= @@ -127,7 +137,9 @@ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= @@ -138,6 +150,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -171,13 +184,16 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb h1:LCMfzVg3sflxTs4UvuP4D8CkoZnfHLe2qzqgDn/4OHs= +github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= @@ -204,6 +220,7 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -215,11 +232,13 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= @@ -234,17 +253,20 @@ github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGp github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -268,7 +290,9 @@ github.com/operator-framework/rukpak v0.18.0/go.mod h1:Yargp8F2i2sCBbxpjHDmtmttY github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -285,6 +309,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -317,21 +342,32 @@ github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vmware-tanzu/carvel-kapp-controller v0.50.0 h1:TCqaH+7OzhgHlD6DZphfu27hCefBnxqAPZyV/2NYZ2g= +github.com/vmware-tanzu/carvel-kapp-controller v0.50.0/go.mod h1:9RwhTFRNwsiLMoIJ6kz5R/EbOXkb+beNJ8y5fyU72Pc= +github.com/vmware-tanzu/carvel-vendir v0.36.0 h1:F9FNk2YysC6DlUDP2Nl2ynsv6JH8S0FYT4OK6HrRco0= +github.com/vmware-tanzu/carvel-vendir v0.36.0/go.mod h1:rPGI/zItMK4QgLRpLix2aykoYufavHyKqqLTONXb2uE= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 h1:p7OofyZ509h8DmPLh8Hn+EIIZm/xYhdZHJ9GnXHdr6U= +github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.7 h1:4DTF1WOM2ZZS/xMOkTFBOcb6XiHu/PKn3rVo6dbewQE= +github.com/yvasiyarov/gorelic v0.0.7/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 h1:AsFN8kXcCVkUFHyuzp1FtYbzp1nCO/H6+1uPSGEyPzM= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 h1:RsQi0qJ2imFfCvZabqzM9cNXBG8k6gXMv1A0cXRmH6A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= @@ -349,7 +385,9 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -502,6 +540,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= @@ -523,6 +562,7 @@ k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdz k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/letsencrypt v0.0.3 h1:H7xDfhkaFFSYEJlKeq38RwX2jYcnTeHuDQyT+mMNMwM= +rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= diff --git a/internal/controllers/extension_controller.go b/internal/controllers/extension_controller.go index 2a3f58964..9b11df20c 100644 --- a/internal/controllers/extension_controller.go +++ b/internal/controllers/extension_controller.go @@ -18,12 +18,25 @@ package controllers import ( "context" + "errors" + "fmt" + "strings" "github.com/go-logr/logr" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/solver" + "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" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -31,17 +44,25 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" "github.com/operator-framework/operator-controller/pkg/features" ) // ExtensionReconciler reconciles a Extension object type ExtensionReconciler struct { client.Client + BundleProvider BundleProvider + Resolver *solver.Solver + HasKappApis bool } +var errkappAPIUnavailable = errors.New("kapp-controller apis unavailable on cluster") + //+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 +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/status,verbs=get;update;patch //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/finalizers,verbs=update +//+kubebuilder:rbac:groups=kappctrl.k14s.io,resources=apps,verbs=get;list;watch;create;update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -116,6 +137,16 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext return ctrl.Result{}, nil } + if !r.HasKappApis { + 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 + } + // Don't do anything if Paused ext.Status.Paused = ext.Spec.Paused if ext.Spec.Paused { @@ -123,22 +154,84 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext return ctrl.Result{}, nil } - // TODO: kapp-controller integration - // * gather variables for resolution - // * perform resolution - // * lookup the bundle in the solution/selection that corresponds to the Extension's desired Source - // * set the status of the Extension based on the respective deployed application status conditions. + // TODO: Instead of using a SAT solver and generating variables based on installed and + // available bundles, work on a filtering mechanism to evaluate if the bundle and its + // APIs are installed on a cluster or not. Feature tracked in a separate issue. + vars, err := r.variables(ctx) + if err != nil { + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted due to failure to gather data for resolution", ext.GetGeneration()) + ext.Status.ResolvedBundleResource = "" + setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted due to failure to gather data for resolution", ext.GetGeneration()) + return ctrl.Result{}, err + } - // Set the TypeInstalled condition to Unknown to indicate that the resolution - // hasn't been attempted yet, due to the spec being invalid. - ext.Status.InstalledBundleResource = "" - setInstalledStatusConditionUnknown(&ext.Status.Conditions, "the Extension interface is not fully implemented", ext.GetGeneration()) - // Set the TypeResolved condition to Unknown to indicate that the resolution - // hasn't been attempted yet, due to the spec being invalid. - ext.Status.ResolvedBundleResource = "" - setResolvedStatusConditionUnknown(&ext.Status.Conditions, "the Extension interface is not fully implemented", ext.GetGeneration()) + // run resolution + selection, err := r.Resolver.Solve(vars) + if err != nil { + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) + ext.Status.ResolvedBundleResource = "" + setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) + return ctrl.Result{}, err + } - setDeprecationStatusesUnknown(&ext.Status.Conditions, "the Extension interface is not fully implemented", ext.GetGeneration()) + // lookup the bundle in the solution that corresponds to the + // ClusterExtension's desired package name. + bundle, err := r.bundleFromSolution(selection, ext.Spec.Source.Package.Name) + if err != nil { + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) + ext.Status.ResolvedBundleResource = "" + setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) + return ctrl.Result{}, err + } + + // Now we can set the Resolved Condition, and the resolvedBundleSource field to the bundle.Image value. + ext.Status.ResolvedBundleResource = bundle.Image + setResolvedStatusConditionSuccess(&ext.Status.Conditions, fmt.Sprintf("resolved to %q", bundle.Image), ext.GetGeneration()) + + mediaType, err := bundle.MediaType() + if err != nil { + setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) + return ctrl.Result{}, err + } + + // TODO: this needs to include the registryV1 bundle option. As of this PR, this only supports direct + // installation of a set of manifests. + if mediaType != catalogmetadata.MediaTypePlain { + // Set the TypeInstalled condition to Unknown to indicate that the resolution + // hasn't been attempted yet, due to the spec being invalid. + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, fmt.Sprintf("bundle type %s not supported currently", mediaType), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) + return ctrl.Result{}, nil + } + + app := r.GenerateExpectedApp(*ext, bundle.Image) + if err := r.ensureApp(ctx, app); err != nil { + // originally Reason: ocv1alpha1.ReasonInstallationFailed + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + return ctrl.Result{}, err + } + + // Converting into structured so that we can map the relevant status to Extension. + existingTypedApp := &kappctrlv1alpha1.App{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(app.UnstructuredContent(), existingTypedApp); err != nil { + // originally Reason: ocv1alpha1.ReasonInstallationStatusUnknown + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) + return ctrl.Result{}, err + } + + mapAppStatusToInstalledCondition(existingTypedApp, ext, bundle.Image) + SetDeprecationStatusInExtension(ext, bundle) return ctrl.Result{}, nil } @@ -156,10 +249,161 @@ func (r *ExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&ocv1alpha1.Extension{}). + Owns(&kappctrlv1alpha1.App{}). Watches(&catalogd.Catalog{}, handler.EnqueueRequestsFromMapFunc(extensionRequestsForCatalog(mgr.GetClient(), mgr.GetLogger()))). Complete(r) } +// TODO: follow up with mapping of all the available App statuses: https://github.com/carvel-dev/kapp-controller/blob/855063edee53315811a13ee8d5df1431ba258ede/pkg/apis/kappctrl/v1alpha1/status.go#L28-L35 +// mapAppStatusToInstalledCondition currently maps only the installed condition. +func mapAppStatusToInstalledCondition(existingApp *kappctrlv1alpha1.App, ext *ocv1alpha1.Extension, bundleImage string) { + appReady := findStatusCondition(existingApp.Status.GenericStatus.Conditions, kappctrlv1alpha1.ReconcileSucceeded) + if appReady == nil { + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&ext.Status.Conditions, "install status unknown", ext.Generation) + return + } + + if appReady.Status != corev1.ConditionTrue { + ext.Status.InstalledBundleResource = "" + setInstalledStatusConditionFailed( + &ext.Status.Conditions, + appReady.Message, + ext.GetGeneration(), + ) + return + } + + // InstalledBundleResource this should be converted into a slice as App allows fetching + // from multiple sources. + ext.Status.InstalledBundleResource = bundleImage + setInstalledStatusConditionSuccess(&ext.Status.Conditions, appReady.Message, ext.Generation) +} + +// setDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension +// based on the provided bundle +func SetDeprecationStatusInExtension(ext *ocv1alpha1.Extension, bundle *catalogmetadata.Bundle) { + // reset conditions to false + conditionTypes := []string{ + ocv1alpha1.TypeDeprecated, + ocv1alpha1.TypePackageDeprecated, + ocv1alpha1.TypeChannelDeprecated, + ocv1alpha1.TypeBundleDeprecated, + } + + for _, conditionType := range conditionTypes { + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: conditionType, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + Message: "", + ObservedGeneration: ext.Generation, + }) + } + + // There are two early return scenarios here: + // 1) The bundle is not deprecated (i.e bundle deprecations) + // AND there are no other deprecations associated with the bundle + // 2) The bundle is not deprecated, there are deprecations associated + // with the bundle (i.e at least one channel the bundle is present in is deprecated OR whole package is deprecated), + // and the ClusterExtension does not specify a channel. This is because the channel deprecations + // are a loose deprecation coupling on the bundle. A ClusterExtension installation is only + // considered deprecated by a channel deprecation when a deprecated channel is specified via + // the spec.channel field. + if (!bundle.IsDeprecated() && !bundle.HasDeprecation()) || (!bundle.IsDeprecated() && ext.Spec.Source.Package.Channel == "") { + return + } + + deprecationMessages := []string{} + + for _, deprecation := range bundle.Deprecations { + switch deprecation.Reference.Schema { + case declcfg.SchemaPackage: + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + case declcfg.SchemaChannel: + if ext.Spec.Source.Package.Channel != deprecation.Reference.Name { + continue + } + + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + case declcfg.SchemaBundle: + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + } + + deprecationMessages = append(deprecationMessages, deprecation.Message) + } + + if len(deprecationMessages) > 0 { + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: strings.Join(deprecationMessages, ";"), + ObservedGeneration: ext.Generation, + }) + } +} + +// findStatusCondition finds the conditionType in conditions. +// TODO: suggest using upstream conditions to Carvel. +func findStatusCondition(conditions []kappctrlv1alpha1.Condition, conditionType kappctrlv1alpha1.ConditionType) *kappctrlv1alpha1.Condition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + return nil +} + +func (r *ExtensionReconciler) ensureApp(ctx context.Context, desiredApp *unstructured.Unstructured) error { + existingApp, err := r.existingAppUnstructured(ctx, desiredApp.GetName(), desiredApp.GetNamespace()) + if client.IgnoreNotFound(err) != nil { + return err + } + + // If the existing BD already has everything that the desired BD has, no need to contact the API server. + // Make sure the status of the existingBD from the server is as expected. + if equality.Semantic.DeepDerivative(desiredApp, existingApp) { + *desiredApp = *existingApp + return nil + } + + return r.Client.Patch(ctx, desiredApp, client.Apply, client.ForceOwnership, client.FieldOwner("operator-controller")) +} + +func (r *ExtensionReconciler) existingAppUnstructured(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) { + existingApp := &kappctrlv1alpha1.App{} + err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, existingApp) + if err != nil { + return nil, err + } + existingApp.APIVersion = "kappctrl.k14s.io/v1alpha1" + existingApp.Kind = "App" + unstrExistingAppObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(existingApp) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: unstrExistingAppObj}, nil +} + // Generate reconcile requests for all extensions affected by a catalog change func extensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.MapFunc { return func(ctx context.Context, _ client.Object) []reconcile.Request { @@ -182,3 +426,89 @@ func extensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.Ma return requests } } + +func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundlePath 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 + // unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically + // identical to "kubectl apply -f" + spec := map[string]interface{}{ + "serviceAccountName": o.Spec.ServiceAccountName, + "fetch": []interface{}{ + map[string]interface{}{ + "image": map[string]interface{}{ + "url": bundlePath, + }, + }, + }, + "template": []interface{}{ + map[string]interface{}{ + "ytt": map[string]interface{}{}, + }, + }, + "deploy": []interface{}{ + map[string]interface{}{ + "kapp": map[string]interface{}{}, + }, + }, + } + + app := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kappctrl.k14s.io/v1alpha1", + "kind": "App", + "metadata": map[string]interface{}{ + "name": o.GetName(), + "namespace": o.GetNamespace(), + }, + "spec": spec, + }, + } + + app.SetOwnerReferences([]metav1.OwnerReference{ + { + APIVersion: ocv1alpha1.GroupVersion.String(), + Kind: "Extension", + Name: o.Name, + UID: o.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }) + return app +} + +func (r *ExtensionReconciler) variables(ctx context.Context) ([]deppy.Variable, error) { + allBundles, err := r.BundleProvider.Bundles(ctx) + if err != nil { + return nil, err + } + + extensionList := ocv1alpha1.ExtensionList{} + if err := r.Client.List(ctx, &extensionList); err != nil { + return nil, err + } + + // Filter apps belogining to current namespace only to create install variables? + // This would ignore cluster scoped APIs installed from other Apps. + appList := kappctrlv1alpha1.AppList{} + if err := r.Client.List(ctx, &appList); err != nil { + return nil, err + } + + return GenerateVariablesForApp(allBundles, extensionList.Items, appList.Items) +} + +func (r *ExtensionReconciler) bundleFromSolution(selection []deppy.Variable, packageName string) (*catalogmetadata.Bundle, error) { + for _, variable := range selection { + switch v := variable.(type) { + case *olmvariables.BundleVariable: + bundlePkgName := v.Bundle().Package + if packageName == bundlePkgName { + return v.Bundle(), nil + } + } + } + return nil, fmt.Errorf("bundle for package %q not found in solution", packageName) +} \ No newline at end of file diff --git a/internal/controllers/variables.go b/internal/controllers/variables.go index d03f8bcae..85a614ffd 100644 --- a/internal/controllers/variables.go +++ b/internal/controllers/variables.go @@ -23,6 +23,7 @@ import ( ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/catalogmetadata" "github.com/operator-framework/operator-controller/internal/resolution/variablesources" + kappctrlv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" ) func GenerateVariables(allBundles []*catalogmetadata.Bundle, clusterExtensions []ocv1alpha1.ClusterExtension, bundleDeployments []rukpakv1alpha2.BundleDeployment) ([]deppy.Variable, error) { @@ -58,3 +59,37 @@ func GenerateVariables(allBundles []*catalogmetadata.Bundle, clusterExtensions [ } return result, nil } + +func GenerateVariablesForApp(allBundles []*catalogmetadata.Bundle, extensions []ocv1alpha1.Extension, apps []kappctrlv1alpha1.App) ([]deppy.Variable, error) { + requiredPackages, err := variablesources.MakeRequiredPackageVariablesForExtensions(allBundles, extensions) + if err != nil { + return nil, err + } + + installedPackages, err := variablesources.MakeInstalledPackageVariablesForExtension(allBundles, extensions, apps) + if err != nil { + return nil, err + } + + bundles, err := variablesources.MakeBundleVariables(allBundles, requiredPackages, installedPackages) + if err != nil { + return nil, err + } + + bundleUniqueness := variablesources.MakeBundleUniquenessVariables(bundles) + + result := []deppy.Variable{} + for _, v := range requiredPackages { + result = append(result, v) + } + for _, v := range installedPackages { + result = append(result, v) + } + for _, v := range bundles { + result = append(result, v) + } + for _, v := range bundleUniqueness { + result = append(result, v) + } + return result, nil +} \ No newline at end of file diff --git a/internal/resolution/variablesources/installed_package.go b/internal/resolution/variablesources/installed_package.go index 339bded3b..736db48f1 100644 --- a/internal/resolution/variablesources/installed_package.go +++ b/internal/resolution/variablesources/installed_package.go @@ -15,6 +15,7 @@ import ( catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" "github.com/operator-framework/operator-controller/pkg/features" + kappctrlv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" ) // MakeInstalledPackageVariables returns variables representing packages @@ -146,3 +147,91 @@ func mapOwnerIDToBundleDeployment(bundleDeployments []rukpakv1alpha2.BundleDeplo return result } + +func mapOwnerIDToApp(apps []kappctrlv1alpha1.App) map[types.UID]*kappctrlv1alpha1.App { + result := map[types.UID]*kappctrlv1alpha1.App{} + + for idx := range apps { + for _, ref := range apps[idx].OwnerReferences { + result[ref.UID] = &apps[idx] + } + } + + return result +} + +// MakeInstalledPackageVariablesForExtension returns variables representing packages +// already installed in the system. +// Meaning that each App managed by operator-controller +// has own variable. +func MakeInstalledPackageVariablesForExtension( + allBundles []*catalogmetadata.Bundle, + extensions []ocv1alpha1.Extension, + apps []kappctrlv1alpha1.App, +) ([]*olmvariables.InstalledPackageVariable, error) { + var successors successorsFunc = legacySemanticsSuccessors + if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) { + successors = semverSuccessors + } + + ownerIDToBundleDeployment := mapOwnerIDToApp(apps) + + result := make([]*olmvariables.InstalledPackageVariable, 0, len(extensions)) + processed := sets.Set[string]{} + for _, extension := range extensions { + if extension.Spec.Source.Package.UpgradeConstraintPolicy == ocv1alpha1.UpgradeConstraintPolicyIgnore { + continue + } + + app, ok := ownerIDToBundleDeployment[extension.UID] + if !ok { + // This can happen when an ClusterExtension is requested, + // but not yet installed (e.g. no BundleDeployment created for it) + continue + } + + // Should not happen, validations in place. + if len(app.Spec.Fetch) == 0 { + return nil, fmt.Errorf("no source defined in App CR %v", app.Name) + } + + // TODO: Very risky. Assumes only one source and the first argument is always an image. + // Extension api needs to be modified to accept multiple sources. + sourceImage := app.Spec.Fetch[0].Image + if sourceImage == nil || sourceImage.URL == "" { + continue + } + + if processed.Has(sourceImage.URL) { + continue + } + processed.Insert(sourceImage.URL) + + bundleImage := sourceImage.URL + + // find corresponding bundle for the installed content + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And( + catalogfilter.WithPackageName(extension.Spec.Source.Package.Name), + catalogfilter.WithBundleImage(bundleImage), + )) + if len(resultSet) == 0 { + return nil, fmt.Errorf("bundle with image %q for package %q not found in available catalogs but is currently installed via BundleDeployment %q", bundleImage, extension.Spec.Source.Package.Name, app.Name) + } + + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByVersion(resultSet[i], resultSet[j]) + }) + installedBundle := resultSet[0] + + upgradeEdges, err := successors(allBundles, installedBundle) + if err != nil { + return nil, err + } + + // you can always upgrade to yourself, i.e. not upgrade + upgradeEdges = append(upgradeEdges, installedBundle) + result = append(result, olmvariables.NewInstalledPackageVariable(installedBundle.Package, upgradeEdges)) + } + + return result, nil +} \ No newline at end of file diff --git a/internal/resolution/variablesources/required_package.go b/internal/resolution/variablesources/required_package.go index 2f23a6062..6f004e393 100644 --- a/internal/resolution/variablesources/required_package.go +++ b/internal/resolution/variablesources/required_package.go @@ -65,3 +65,56 @@ func MakeRequiredPackageVariables(allBundles []*catalogmetadata.Bundle, clusterE return result, nil } + +// MakeRequiredPackageVariablesForExtensions returns a variable which represent +// explicit requirement for a package from an user. +// This is when a user explicitly asks "install this" via Extension API. +func MakeRequiredPackageVariablesForExtensions(allBundles []*catalogmetadata.Bundle, extensions []ocv1alpha1.Extension) ([]*olmvariables.RequiredPackageVariable, error) { + result := make([]*olmvariables.RequiredPackageVariable, 0, len(extensions)) + + for _, extension := range extensions { + packageName := extension.Spec.Source.Package.Name + channelName := extension.Spec.Source.Package.Channel + versionRange := extension.Spec.Source.Package.Version + + predicates := []catalogfilter.Predicate[catalogmetadata.Bundle]{ + catalogfilter.WithPackageName(packageName), + } + + if channelName != "" { + predicates = append(predicates, catalogfilter.InChannel(channelName)) + } + + if versionRange != "" { + vr, err := mmsemver.NewConstraint(versionRange) + if err != nil { + return nil, fmt.Errorf("invalid version range %q: %w", versionRange, err) + } + predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) + } + + 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) + } + if versionRange != "" { + return nil, fmt.Errorf("no package %q matching version %q found", packageName, versionRange) + } + if channelName != "" { + return nil, fmt.Errorf("no package %q found in channel %q", packageName, channelName) + } + return nil, fmt.Errorf("no package %q found", packageName) + } + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByVersion(resultSet[i], resultSet[j]) + }) + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByDeprecated(resultSet[i], resultSet[j]) + }) + + result = append(result, olmvariables.NewRequiredPackageVariable(packageName, resultSet)) + } + + return result, nil +} \ No newline at end of file