diff --git a/cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json b/cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json index 63d017cfaa3..7e7d5ea28f8 100644 --- a/cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json +++ b/cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json @@ -439,7 +439,7 @@ "PackagesService" ] }, - "patch": { + "put": { "operationId": "PackagesService_UpdateInstalledPackage", "responses": { "200": { @@ -963,7 +963,7 @@ "FluxV2PackagesService" ] }, - "patch": { + "put": { "summary": "UpdateInstalledPackage updates an installed package based on the request.", "operationId": "FluxV2PackagesService_UpdateInstalledPackage", "responses": { @@ -1505,7 +1505,7 @@ "HelmPackagesService" ] }, - "patch": { + "put": { "summary": "UpdateInstalledPackage updates an installed package based on the request.", "operationId": "HelmPackagesService_UpdateInstalledPackage", "responses": { @@ -2004,7 +2004,7 @@ "KappControllerPackagesService" ] }, - "patch": { + "put": { "summary": "UpdateInstalledPackage updates an installed package based on the request.", "operationId": "KappControllerPackagesService_UpdateInstalledPackage", "responses": { @@ -2931,7 +2931,7 @@ }, "pkgVersionReference": { "$ref": "#/definitions/v1alpha1VersionReference", - "title": "For helm this will be the exact version in VersionReference.version\nFor other plugins we can extend the VersionReference as needed. Optional" + "title": "For helm this will be the exact version in VersionReference.version\nFor fluxv2 this could be any semver constraint expression\nFor other plugins we can extend the VersionReference as needed. Optional" }, "values": { "type": "string", @@ -2942,7 +2942,7 @@ "description": "An optional field for specifying data common to systems that reconcile\nthe package on the cluster." } }, - "description": "Request for UpdateInstalledPackage. Partial resource updates are supported.\nFor example, to change the package version one only needs to specify the version reference.\nSimilarly to update the values, one only needs to specify that field", + "description": "Request for UpdateInstalledPackage. The intent is to reach the desired state specified\nby the fields in the request, while leaving other fields intact. This is a whole \nobject \"Update\" semantics rather than \"Patch\" semantics. The caller will provide the \nvalues for the fields fields below, which will replace, or be overlayed onto, the \ncorresponding fields in the existing resource. For example, with the\nUpdateInstalledPackageRequest, it is not possible to change just the 'package version \nreference' without also specifying 'values' field. As a side effect, not specifying the \n'values' field in the request means there are no values specified in the desired state. \nSo the meaning of each field value is describing the desired state of the corresponding \nfield in the resource after the update operation has completed the renconciliation.", "title": "UpdateInstalledPackageRequest" }, "v1alpha1UpdateInstalledPackageResponse": { diff --git a/cmd/kubeapps-apis/gen/apidocs.pb.go b/cmd/kubeapps-apis/gen/apidocs.pb.go index ba692fe6b39..1c20c24ec02 100644 --- a/cmd/kubeapps-apis/gen/apidocs.pb.go +++ b/cmd/kubeapps-apis/gen/apidocs.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/apidocs/v1alpha1/apidocs.proto diff --git a/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.go b/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.go index 4851089bc06..0aef4e95763 100644 --- a/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.go +++ b/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/core/packages/v1alpha1/packages.proto @@ -494,9 +494,16 @@ func (x *CreateInstalledPackageRequest) GetReconciliationOptions() *Reconciliati // UpdateInstalledPackageRequest // -// Request for UpdateInstalledPackage. Partial resource updates are supported. -// For example, to change the package version one only needs to specify the version reference. -// Similarly to update the values, one only needs to specify that field +// Request for UpdateInstalledPackage. The intent is to reach the desired state specified +// by the fields in the request, while leaving other fields intact. This is a whole +// object "Update" semantics rather than "Patch" semantics. The caller will provide the +// values for the fields fields below, which will replace, or be overlayed onto, the +// corresponding fields in the existing resource. For example, with the +// UpdateInstalledPackageRequest, it is not possible to change just the 'package version +// reference' without also specifying 'values' field. As a side effect, not specifying the +// 'values' field in the request means there are no values specified in the desired state. +// So the meaning of each field value is describing the desired state of the corresponding +// field in the resource after the update operation has completed the renconciliation. type UpdateInstalledPackageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -506,6 +513,7 @@ type UpdateInstalledPackageRequest struct { // Required InstalledPackageRef *InstalledPackageReference `protobuf:"bytes,1,opt,name=installed_package_ref,json=installedPackageRef,proto3" json:"installed_package_ref,omitempty"` // For helm this will be the exact version in VersionReference.version + // For fluxv2 this could be any semver constraint expression // For other plugins we can extend the VersionReference as needed. Optional PkgVersionReference *VersionReference `protobuf:"bytes,2,opt,name=pkg_version_reference,json=pkgVersionReference,proto3" json:"pkg_version_reference,omitempty"` // An optional serialized values string to be included when templating a @@ -3112,7 +3120,7 @@ var file_kubeappsapis_core_packages_v1alpha1_packages_proto_rawDesc = []byte{ 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x34, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2e, 0x32, 0x29, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, + 0x34, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2e, 0x1a, 0x29, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0xd4, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, diff --git a/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.gw.go b/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.gw.go index 53ea00f2b5e..962388366e1 100644 --- a/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.gw.go +++ b/cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.gw.go @@ -459,7 +459,7 @@ func RegisterPackagesServiceHandlerServer(ctx context.Context, mux *runtime.Serv }) - mux.Handle("PATCH", pattern_PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -666,7 +666,7 @@ func RegisterPackagesServiceHandlerClient(ctx context.Context, mux *runtime.Serv }) - mux.Handle("PATCH", pattern_PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) diff --git a/cmd/kubeapps-apis/gen/core/plugins/v1alpha1/plugins.pb.go b/cmd/kubeapps-apis/gen/core/plugins/v1alpha1/plugins.pb.go index 7b5ff542fd3..2a2b48e61c5 100644 --- a/cmd/kubeapps-apis/gen/core/plugins/v1alpha1/plugins.pb.go +++ b/cmd/kubeapps-apis/gen/core/plugins/v1alpha1/plugins.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/core/plugins/v1alpha1/plugins.proto diff --git a/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.go b/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.go index 5935080d675..3e4dfa39fa0 100644 --- a/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.go +++ b/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/plugins/fluxv2/packages/v1alpha1/fluxv2.proto @@ -391,7 +391,7 @@ var file_kubeappsapis_plugins_fluxv2_packages_v1alpha1_fluxv2_proto_rawDesc = [] 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x38, 0x32, 0x33, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x38, 0x1a, 0x33, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x66, 0x6c, 0x75, 0x78, 0x76, 0x32, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0xde, diff --git a/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.gw.go b/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.gw.go index fbdc14a48a5..5f543555fda 100644 --- a/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.gw.go +++ b/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1/fluxv2.pb.gw.go @@ -519,7 +519,7 @@ func RegisterFluxV2PackagesServiceHandlerServer(ctx context.Context, mux *runtim }) - mux.Handle("PATCH", pattern_FluxV2PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_FluxV2PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -746,7 +746,7 @@ func RegisterFluxV2PackagesServiceHandlerClient(ctx context.Context, mux *runtim }) - mux.Handle("PATCH", pattern_FluxV2PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_FluxV2PackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) diff --git a/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.go b/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.go index 32cc551618c..e389bea43a9 100644 --- a/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.go +++ b/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/plugins/helm/packages/v1alpha1/helm.proto @@ -206,7 +206,7 @@ var file_kubeappsapis_plugins_helm_packages_v1alpha1_helm_proto_rawDesc = []byte 0x61, 0x67, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3c, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x36, 0x32, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x68, + 0xe4, 0x93, 0x02, 0x36, 0x1a, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x68, 0x65, 0x6c, 0x6d, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0xdc, 0x01, 0x0a, 0x16, 0x44, diff --git a/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.gw.go b/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.gw.go index ab336f1efac..31d1259a37c 100644 --- a/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.gw.go +++ b/cmd/kubeapps-apis/gen/plugins/helm/packages/v1alpha1/helm.pb.gw.go @@ -460,7 +460,7 @@ func RegisterHelmPackagesServiceHandlerServer(ctx context.Context, mux *runtime. }) - mux.Handle("PATCH", pattern_HelmPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_HelmPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -667,7 +667,7 @@ func RegisterHelmPackagesServiceHandlerClient(ctx context.Context, mux *runtime. }) - mux.Handle("PATCH", pattern_HelmPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_HelmPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) diff --git a/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.go b/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.go index 03d1b1723ee..c3752bb2a7b 100644 --- a/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.go +++ b/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.27.1 // protoc v3.17.3 // source: kubeappsapis/plugins/kapp_controller/packages/v1alpha1/kapp_controller.proto @@ -399,7 +399,7 @@ var file_kubeappsapis_plugins_kapp_controller_packages_v1alpha1_kapp_controller_ 0x67, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x41, 0x32, 0x3c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x6b, 0x61, + 0x93, 0x02, 0x41, 0x1a, 0x3c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x6b, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, diff --git a/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.gw.go b/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.gw.go index 0a49373c828..4696158b14c 100644 --- a/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.gw.go +++ b/cmd/kubeapps-apis/gen/plugins/kapp_controller/packages/v1alpha1/kapp_controller.pb.gw.go @@ -519,7 +519,7 @@ func RegisterKappControllerPackagesServiceHandlerServer(ctx context.Context, mux }) - mux.Handle("PATCH", pattern_KappControllerPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_KappControllerPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -746,7 +746,7 @@ func RegisterKappControllerPackagesServiceHandlerClient(ctx context.Context, mux }) - mux.Handle("PATCH", pattern_KappControllerPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("PUT", pattern_KappControllerPackagesService_UpdateInstalledPackage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go new file mode 100644 index 00000000000..14a33759453 --- /dev/null +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go @@ -0,0 +1,344 @@ +/* +Copyright © 2021 VMware +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "context" + "fmt" + "math/rand" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + plugins "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/plugins/v1alpha1" + fluxplugin "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1" + "google.golang.org/grpc" + kubecorev1 "k8s.io/api/core/v1" + kuberbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + // EnvvarFluxIntegrationTests enables tests that run against a local kind cluster + envVarFluxIntegrationTests = "ENABLE_FLUX_INTEGRATION_TESTS" +) + +func checkEnv(t *testing.T) fluxplugin.FluxV2PackagesServiceClient { + enableEnvVar := os.Getenv(envVarFluxIntegrationTests) + runTests := false + if enableEnvVar != "" { + var err error + runTests, err = strconv.ParseBool(enableEnvVar) + if err != nil { + t.Fatalf("%+v", err) + } + } + + if !runTests { + t.Skipf("skipping flux plugin integration tests as %q not set to be true", envVarFluxIntegrationTests) + } else { + if up, err := isLocalKindClusterUp(t); err != nil || !up { + t.Fatalf("Failed to find local kind cluster due to: [%v]", err) + } + var fluxPluginClient fluxplugin.FluxV2PackagesServiceClient + var err error + if fluxPluginClient, err = getFluxPluginClient(t); err != nil { + t.Fatalf("Failed to get fluxv2 plugin due to: [%v]", err) + } + rand.Seed(time.Now().UnixNano()) + return fluxPluginClient + } + return nil +} + +func isLocalKindClusterUp(t *testing.T) (up bool, err error) { + t.Logf("+isLocalKindClusterUp") + cmd := exec.Command("kind", "get", "clusters") + bytes, err := cmd.CombinedOutput() + if err != nil { + t.Logf("%s", string(bytes)) + return false, err + } + if !strings.Contains(string(bytes), "kubeapps\n") { + return false, nil + } + + // naively assume that if the api server reports nodes, the cluster is up + typedClient, err := kubeGetTypedClient() + if err != nil { + t.Logf("%s", string(bytes)) + return false, err + } + + nodeList, err := typedClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + t.Logf("%s", string(bytes)) + return false, err + } + + if len(nodeList.Items) == 1 || nodeList.Items[0].Name == "node/kubeapps-control-plane" { + return true, nil + } else { + return false, fmt.Errorf("Unexpected cluster nodes: [%v]", nodeList) + } +} + +func getFluxPluginClient(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, error) { + t.Logf("+getFluxPluginClient") + + var opts []grpc.DialOption + opts = append(opts, grpc.WithInsecure()) + opts = append(opts, grpc.WithBlock()) + target := "localhost:8080" + conn, err := grpc.Dial(target, opts...) + if err != nil { + t.Fatalf("failed to dial [%s] due to: %v", target, err) + } + t.Cleanup(func() { conn.Close() }) + pluginsCli := plugins.NewPluginsServiceClient(conn) + response, err := pluginsCli.GetConfiguredPlugins(context.TODO(), &plugins.GetConfiguredPluginsRequest{}) + if err != nil { + t.Fatalf("failed to GetConfiguredPlugins due to: %v", err) + } + found := false + for _, p := range response.Plugins { + if p.Name == "fluxv2.packages" && p.Version == "v1alpha1" { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("kubeapps Flux v2 plugin is not registered") + } + return fluxplugin.NewFluxV2PackagesServiceClient(conn), nil +} + +// This should eventually be replaced with fluxPlugin CreateRepository() call as soon as we finalize +// the design +func kubeCreateHelmRepository(t *testing.T, name, url, namespace string) error { + t.Logf("+kubeCreateHelmRepository(%s,%s)", name, namespace) + unstructuredRepo := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": fmt.Sprintf("%s/%s", fluxGroup, fluxVersion), + "kind": fluxHelmRepository, + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "url": url, + "interval": "1m", + }, + }, + } + + if ifc, err := kubeGetHelmRepositoryResourceInterface(namespace); err != nil { + return err + } else if _, err = ifc.Create(context.TODO(), &unstructuredRepo, metav1.CreateOptions{}); err != nil { + return err + } + return nil +} + +// this should eventually be replaced with flux plugin's DeleteRepository() +func kubeDeleteHelmRepository(t *testing.T, name, namespace string) error { + t.Logf("+kubeDeleteHelmRepository(%s,%s)", name, namespace) + if ifc, err := kubeGetHelmRepositoryResourceInterface(namespace); err != nil { + return err + } else if err = ifc.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +// this should eventually be replaced with flux plugin's DeleteInstalledPackage() +func kubeDeleteHelmRelease(t *testing.T, name, namespace string) error { + t.Logf("+kubeDeleteHelmRelease(%s,%s)", name, namespace) + if ifc, err := kubeGetHelmReleaseResourceInterface(namespace); err != nil { + return err + // remove finalizer on HelmRelease cuz sometimes it gets stuck indefinitely + } else if _, err = ifc.Patch(context.TODO(), name, types.JSONPatchType, + []byte("[ { \"op\": \"remove\", \"path\": \"/metadata/finalizers\" } ]"), metav1.PatchOptions{}); err != nil { + return err + } else if err = ifc.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +func kubeGetPodNames(t *testing.T, namespace string) (names []string, err error) { + t.Logf("+kubeGetPodNames(%s)", namespace) + if typedClient, err := kubeGetTypedClient(); err != nil { + return nil, err + } else if podList, err := typedClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}); err != nil { + return nil, err + } else { + names := []string{} + for _, p := range podList.Items { + names = append(names, p.GetName()) + } + return names, nil + } +} + +// will create a service account with cluster-admin privs +func kubeCreateServiceAccount(t *testing.T, name, namespace string) error { + t.Logf("+kubeCreateServiceAccount(%s,%s)", name, namespace) + if typedClient, err := kubeGetTypedClient(); err != nil { + return err + } else if _, err = typedClient.CoreV1().ServiceAccounts(namespace).Create( + context.TODO(), + &kubecorev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + metav1.CreateOptions{}); err != nil { + return err + } else if _, err = typedClient.RbacV1().ClusterRoleBindings().Create( + context.TODO(), + &kuberbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name + "-binding", + }, + Subjects: []kuberbacv1.Subject{ + { + Kind: kuberbacv1.ServiceAccountKind, + Name: name, + Namespace: namespace, + }, + }, + RoleRef: kuberbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "cluster-admin", + }, + }, + metav1.CreateOptions{}); err != nil { + return err + } + return nil +} + +func kubeDeleteServiceAccount(t *testing.T, name, namespace string) error { + t.Logf("+kubeDeleteServiceAccount(%s,%s)", name, namespace) + if typedClient, err := kubeGetTypedClient(); err != nil { + return err + } else if err = typedClient.RbacV1().ClusterRoleBindings().Delete( + context.TODO(), + name+"-binding", + metav1.DeleteOptions{}); err != nil { + return err + } else if err = typedClient.CoreV1().ServiceAccounts(namespace).Delete( + context.TODO(), + name, + metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +func kubeDeleteNamespace(t *testing.T, namespace string) error { + t.Logf("+kubeDeleteNamespace(%s)", namespace) + if typedClient, err := kubeGetTypedClient(); err != nil { + return err + } else if typedClient.CoreV1().Namespaces().Delete( + context.TODO(), + namespace, + metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +func kubeGetHelmReleaseResourceInterface(namespace string) (dynamic.ResourceInterface, error) { + clientset, err := kubeGetDynamicClient() + if err != nil { + return nil, err + } + relResource := schema.GroupVersionResource{ + Group: fluxHelmReleaseGroup, + Version: fluxHelmReleaseVersion, + Resource: fluxHelmReleases, + } + return clientset.Resource(relResource).Namespace(namespace), nil +} + +func kubeGetHelmRepositoryResourceInterface(namespace string) (dynamic.ResourceInterface, error) { + clientset, err := kubeGetDynamicClient() + if err != nil { + return nil, err + } + repoResource := schema.GroupVersionResource{ + Group: fluxGroup, + Version: fluxVersion, + Resource: fluxHelmRepositories, + } + return clientset.Resource(repoResource).Namespace(namespace), nil +} + +func kubeGetDynamicClient() (dynamic.Interface, error) { + if dynamicClient != nil { + return dynamicClient, nil + } else { + if config, err := restConfig(); err != nil { + return nil, err + } else { + dynamicClient, err = dynamic.NewForConfig(config) + return dynamicClient, err + } + } +} + +func kubeGetTypedClient() (kubernetes.Interface, error) { + if typedClient != nil { + return typedClient, nil + } else { + if config, err := restConfig(); err != nil { + return nil, err + } else { + typedClient, err = kubernetes.NewForConfig(config) + return typedClient, err + } + } +} + +func restConfig() (*rest.Config, error) { + kubeconfig := os.Getenv("KUBECONFIG") + return clientcmd.BuildConfigFromFlags("", kubeconfig) +} + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// global vars +var ( + dynamicClient dynamic.Interface + typedClient kubernetes.Interface + letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") +) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go index b3080480d8d..e22e1526e1d 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go @@ -45,6 +45,8 @@ const ( fluxHelmRelease = "HelmRelease" fluxHelmReleases = "helmreleases" fluxHelmReleaseList = "HelmReleaseList" + + defaultReconcileInterval = "1m" ) func (s *Server) getReleasesResourceInterface(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) { @@ -359,7 +361,10 @@ func (s *Server) newRelease(ctx context.Context, packageRef *corev1.AvailablePac } } - fluxHelmRelease := newFluxHelmRelease(chart, kubeappsNamespace, targetName, versionRef, reconcile, values) + fluxHelmRelease, err := newFluxHelmRelease(chart, kubeappsNamespace, targetName, versionRef, reconcile, values) + if err != nil { + return nil, err + } newRelease, err := resourceIfc.Create(ctx, fluxHelmRelease, metav1.CreateOptions{}) if err != nil { return nil, err @@ -376,14 +381,87 @@ func (s *Server) newRelease(ctx context.Context, packageRef *corev1.AvailablePac }, nil } -func (s *Server) updateRelease(ctx context.Context, packageRef *corev1.InstalledPackageReference, versionRef *corev1.VersionReference, reconcile *corev1.ReconciliationOptions, valuesString string) error { - _, err := s.getReleasesResourceInterface(ctx, packageRef.Context.Namespace) +func (s *Server) updateRelease(ctx context.Context, packageRef *corev1.InstalledPackageReference, versionRef *corev1.VersionReference, reconcile *corev1.ReconciliationOptions, valuesString string) (*corev1.InstalledPackageReference, error) { + ifc, err := s.getReleasesResourceInterface(ctx, packageRef.Context.Namespace) + if err != nil { + return nil, err + } + + unstructuredRel, err := ifc.Get(ctx, packageRef.Identifier, metav1.GetOptions{}) if err != nil { - return err + if errors.IsNotFound(err) { + return nil, status.Errorf(codes.NotFound, "%q", err) + } else { + return nil, status.Errorf(codes.Internal, "%q", err) + } + } + + if versionRef.GetVersion() != "" { + if err = unstructured.SetNestedField(unstructuredRel.Object, versionRef.GetVersion(), "spec", "chart", "spec", "version"); err != nil { + return nil, err + } + } else { + unstructured.RemoveNestedField(unstructuredRel.Object, "spec", "chart", "spec", "version") } - // TODO (gfichtenholt) implement - return nil + if valuesString != "" { + values := make(map[string]interface{}) + if err = yaml.Unmarshal([]byte(valuesString), &values); err != nil { + return nil, err + } else if err = unstructured.SetNestedMap(unstructuredRel.Object, values, "spec", "values"); err != nil { + return nil, err + } + } else { + unstructured.RemoveNestedField(unstructuredRel.Object, "spec", "values") + } + + setInterval, setServiceAccount := false, false + if reconcile != nil { + if reconcile.Interval > 0 { + reconcileInterval := (time.Duration(reconcile.Interval) * time.Second).String() + if err := unstructured.SetNestedField(unstructuredRel.Object, reconcileInterval, "spec", "interval"); err != nil { + return nil, err + } + setInterval = true + } + if reconcile.ServiceAccountName != "" { + if err = unstructured.SetNestedField(unstructuredRel.Object, reconcile.ServiceAccountName, "spec", "serviceAccountName"); err != nil { + setServiceAccount = true + } + } + if err = unstructured.SetNestedField(unstructuredRel.Object, reconcile.Suspend, "spec", "suspend"); err != nil { + return nil, err + } + } + + if !setInterval { + // interval is a required field + if err = unstructured.SetNestedField(unstructuredRel.Object, defaultReconcileInterval, "spec", "interval"); err != nil { + return nil, err + } + } + if !setServiceAccount { + unstructured.RemoveNestedField(unstructuredRel.Object, "spec", "serviceAccountName") + } + + // get rid of the status field, since now there will be a new reconciliation process and the current status no + // longer applies. metadata and spec I want to keep, as they may have had added labels and/or annotations and/or + // even other changes made by the user. + unstructured.RemoveNestedField(unstructuredRel.Object, "status") + + // replace the object in k8s with a new desired state + unstructuredRel, err = ifc.Update(ctx, unstructuredRel, metav1.UpdateOptions{}) + if err != nil { + return nil, err + } + + log.V(4).Infof("Updated release: %s", prettyPrintMap(unstructuredRel.Object)) + + return &corev1.InstalledPackageReference{ + Context: &corev1.Context{Namespace: packageRef.Context.Namespace}, + Identifier: packageRef.Identifier, + Plugin: GetPluginDetail(), + }, nil } // returns 3 things: @@ -504,7 +582,7 @@ func installedPackageAvailablePackageRefFromUnstructured(unstructuredRelease map // 1. spec.chart.spec.sourceRef.namespace, where HelmRepository CRD object referenced exists // 2. metadata.namespace, where this HelmRelease CRD will exist // 3. spec.targetNamespace, where flux will install any artifacts from the release -func newFluxHelmRelease(chart *models.Chart, releaseNamespace string, targetName types.NamespacedName, versionRef *corev1.VersionReference, reconcile *corev1.ReconciliationOptions, values map[string]interface{}) *unstructured.Unstructured { +func newFluxHelmRelease(chart *models.Chart, releaseNamespace string, targetName types.NamespacedName, versionRef *corev1.VersionReference, reconcile *corev1.ReconciliationOptions, values map[string]interface{}) (*unstructured.Unstructured, error) { unstructuredRel := unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": fmt.Sprintf("%s/%s", fluxHelmReleaseGroup, fluxHelmReleaseVersion), @@ -531,24 +609,34 @@ func newFluxHelmRelease(chart *models.Chart, releaseNamespace string, targetName }, }, } - if versionRef != nil && versionRef.Version != "" { - unstructured.SetNestedField(unstructuredRel.Object, versionRef.Version, "spec", "chart", "spec", "version") + if versionRef.GetVersion() != "" { + if err := unstructured.SetNestedField(unstructuredRel.Object, versionRef.GetVersion(), "spec", "chart", "spec", "version"); err != nil { + return nil, err + } } - reconcileInterval := "1m" // unless explictly specified + reconcileInterval := defaultReconcileInterval // unless explictly specified if reconcile != nil { if reconcile.Interval > 0 { reconcileInterval = (time.Duration(reconcile.Interval) * time.Second).String() } - unstructured.SetNestedField(unstructuredRel.Object, reconcile.Suspend, "spec", "suspend") + if err := unstructured.SetNestedField(unstructuredRel.Object, reconcile.Suspend, "spec", "suspend"); err != nil { + return nil, err + } if reconcile.ServiceAccountName != "" { - unstructured.SetNestedField(unstructuredRel.Object, reconcile.ServiceAccountName, "spec", "serviceAccountName") + if err := unstructured.SetNestedField(unstructuredRel.Object, reconcile.ServiceAccountName, "spec", "serviceAccountName"); err != nil { + return nil, err + } } } if values != nil { - unstructured.SetNestedMap(unstructuredRel.Object, values, "spec", "values") + if err := unstructured.SetNestedMap(unstructuredRel.Object, values, "spec", "values"); err != nil { + return nil, err + } } // required fields, without which flux controller will fail to create the CRD - unstructured.SetNestedField(unstructuredRel.Object, reconcileInterval, "spec", "interval") - return &unstructuredRel + if err := unstructured.SetNestedField(unstructuredRel.Object, reconcileInterval, "spec", "interval"); err != nil { + return nil, err + } + return &unstructuredRel, nil } diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go index e5e2dd38241..fd5c0a24ebf 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go @@ -14,11 +14,6 @@ package main import ( "context" - "fmt" - "math/rand" - "os" - "os/exec" - "strconv" "strings" "testing" "time" @@ -28,17 +23,6 @@ import ( corev1 "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1" plugins "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/plugins/v1alpha1" fluxplugin "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1" - "google.golang.org/grpc" - kubecorev1 "k8s.io/api/core/v1" - kuberbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // This is an integration test: it tests the full integration of flux plugin with flux back-end @@ -52,11 +36,9 @@ import ( // 3) run ./kind-cluster-setup.sh once prior to these tests const ( - // EnvvarFluxIntegrationTests enables tests that run against a local kind cluster - envVarFluxIntegrationTests = "ENABLE_FLUX_INTEGRATION_TESTS" - - // the only repo this test uses so far. Enough for this test. This is local copy of the first few entries - // on "https://stefanprodan.github.io/podinfo/index.yaml" on Sept 10 2021. + // the only repo these tests use so far. This is local copy of the first few entries + // on "https://stefanprodan.github.io/podinfo/index.yaml" as of Sept 10 2021 with the chart + // urls modified to link to .tgz files also within the local cluster. // If we want other repos, we'll have add directories and tinker with ./Dockerfile and NGINX conf. // This relies on fluxv2plugin-testdata-svc service stood up by testdata/kind-cluster-setup.sh podinfo_repo_url = "http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80" @@ -123,7 +105,7 @@ type integrationTestUpdateSpec struct { integrationTestCreateSpec request *corev1.UpdateInstalledPackageRequest // this is expected AFTER the update call completes - expectedDetail *corev1.InstalledPackageDetail + expectedDetailAfterUpdate *corev1.InstalledPackageDetail } func TestKindClusterUpdateInstalledPackage(t *testing.T) { @@ -138,14 +120,52 @@ func TestKindClusterUpdateInstalledPackage(t *testing.T) { expectedDetail: expected_detail_podinfo_5_2_1, expectedPodPrefix: "@TARGET_NS@-my-podinfo-", }, - request: &corev1.UpdateInstalledPackageRequest{ - // this will be filled in by the code below after a call to create(...) completes - InstalledPackageRef: nil, - PkgVersionReference: &corev1.VersionReference{ - Version: "6.0.0", - }, + request: update_request_1, + expectedDetailAfterUpdate: expected_detail_podinfo_6_0_0, + }, + { + integrationTestCreateSpec: integrationTestCreateSpec{ + testName: "update test (add values)", + repoUrl: podinfo_repo_url, + request: create_request_podinfo_5_2_1_no_values, + expectedDetail: expected_detail_podinfo_5_2_1_no_values, + expectedPodPrefix: "@TARGET_NS@-my-podinfo-", + }, + request: update_request_2, + expectedDetailAfterUpdate: expected_detail_podinfo_5_2_1_values, + }, + { + integrationTestCreateSpec: integrationTestCreateSpec{ + testName: "update test (change values)", + repoUrl: podinfo_repo_url, + request: create_request_podinfo_5_2_1_values_2, + expectedDetail: expected_detail_podinfo_5_2_1_values_2, + expectedPodPrefix: "@TARGET_NS@-my-podinfo-", + }, + request: update_request_3, + expectedDetailAfterUpdate: expected_detail_podinfo_5_2_1_values_3, + }, + { + integrationTestCreateSpec: integrationTestCreateSpec{ + testName: "update test (remove values)", + repoUrl: podinfo_repo_url, + request: create_request_podinfo_5_2_1_values_4, + expectedDetail: expected_detail_podinfo_5_2_1_values_4, + expectedPodPrefix: "@TARGET_NS@-my-podinfo-", + }, + request: update_request_4, + expectedDetailAfterUpdate: expected_detail_podinfo_5_2_1_values_5, + }, + { + integrationTestCreateSpec: integrationTestCreateSpec{ + testName: "update test (values dont change)", + repoUrl: podinfo_repo_url, + request: create_request_podinfo_5_2_1_values_6, + expectedDetail: expected_detail_podinfo_5_2_1_values_6, + expectedPodPrefix: "@TARGET_NS@-my-podinfo-", }, - expectedDetail: expected_detail_podinfo_5_2_1, + request: update_request_5, + expectedDetailAfterUpdate: expected_detail_podinfo_5_2_1_values_6, }, } @@ -159,31 +179,18 @@ func TestKindClusterUpdateInstalledPackage(t *testing.T) { t.Fatalf("%+v", err) } - /* TODO - var actualDetail *corev1.InstalledPackageDetail - const maxWait = 25 - for i := 0; i <= maxWait; i++ { - resp2, err := fluxPluginClient.GetInstalledPackageDetail( - context.TODO(), - &corev1.GetInstalledPackageDetailRequest{InstalledPackageRef: installedRef}) - if err != nil { - t.Fatalf("%+v", err) - } - - if resp2.InstalledPackageDetail.Status.Ready == true && - resp2.InstalledPackageDetail.Status.Reason == corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED { - actualDetail = resp2.InstalledPackageDetail - break - } - t.Logf("Waiting 1s due to: [%s], userReason: [%s], attempt: [%d/%d]...", - resp2.InstalledPackageDetail.Status.Reason, resp2.InstalledPackageDetail.Status.UserReason, i+1, maxWait) - time.Sleep(1 * time.Second) - } + actualRespAfterUpdate := waitUntilInstallCompletes(t, fluxPluginClient, installedRef, false) - if actualDetail == nil { - t.Fatalf("Timed out waiting for task to complete") + tc.expectedDetailAfterUpdate.PostInstallationNotes = strings.ReplaceAll( + tc.expectedDetailAfterUpdate.PostInstallationNotes, + "@TARGET_NS@", + tc.integrationTestCreateSpec.request.TargetContext.Namespace) + + expectedResp := &corev1.GetInstalledPackageDetailResponse{ + InstalledPackageDetail: tc.expectedDetailAfterUpdate, } - */ + + compareActualVsExpectedGetInstalledPackageDetailResponse(t, actualRespAfterUpdate, expectedResp) }) } } @@ -264,55 +271,17 @@ func createAndWaitForHelmRelease(t *testing.T, tc integrationTestCreateSpec, flu } }) - var actualDetail *corev1.InstalledPackageDetail - for i := 0; i <= maxWait; i++ { - resp2, err := fluxPluginClient.GetInstalledPackageDetail( - context.TODO(), - &corev1.GetInstalledPackageDetailRequest{InstalledPackageRef: installedPackageRef}) - if err != nil { - t.Fatalf("%+v", err) - } - - if !tc.expectInstallFailure { - if resp2.InstalledPackageDetail.Status.Ready == true && - resp2.InstalledPackageDetail.Status.Reason == corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED { - actualDetail = resp2.InstalledPackageDetail - break - } - } else { - if resp2.InstalledPackageDetail.Status.Ready == false && - resp2.InstalledPackageDetail.Status.Reason == corev1.InstalledPackageStatus_STATUS_REASON_FAILED { - actualDetail = resp2.InstalledPackageDetail - break - } - } - t.Logf("Waiting 1s due to: [%s], userReason: [%s], attempt: [%d/%d]...", - resp2.InstalledPackageDetail.Status.Reason, resp2.InstalledPackageDetail.Status.UserReason, i+1, maxWait) - time.Sleep(1 * time.Second) - } - - if actualDetail == nil { - t.Fatalf("Timed out waiting for task to complete") - } + actualResp := waitUntilInstallCompletes(t, fluxPluginClient, installedPackageRef, tc.expectInstallFailure) tc.expectedDetail.PostInstallationNotes = strings.ReplaceAll( tc.expectedDetail.PostInstallationNotes, "@TARGET_NS@", tc.request.TargetContext.Namespace) - opts = cmpopts.IgnoreUnexported( - corev1.GetInstalledPackageDetailResponse{}, - corev1.InstalledPackageDetail{}, - corev1.InstalledPackageReference{}, - corev1.Context{}, - corev1.VersionReference{}, - corev1.InstalledPackageStatus{}, - corev1.PackageAppVersion{}, - plugins.Plugin{}, - corev1.ReconciliationOptions{}, - corev1.AvailablePackageReference{}) - if got, want := actualDetail, tc.expectedDetail; !cmp.Equal(want, got, opts) { - t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts)) + expectedResp := &corev1.GetInstalledPackageDetailResponse{ + InstalledPackageDetail: tc.expectedDetail, } + compareActualVsExpectedGetInstalledPackageDetailResponse(t, actualResp, expectedResp) + if !tc.expectInstallFailure { // check artifacts in target namespace: tc.expectedPodPrefix = strings.ReplaceAll( @@ -331,608 +300,700 @@ func createAndWaitForHelmRelease(t *testing.T, tc integrationTestCreateSpec, flu return installedPackageRef } -func checkEnv(t *testing.T) fluxplugin.FluxV2PackagesServiceClient { - enableEnvVar := os.Getenv(envVarFluxIntegrationTests) - runTests := false - if enableEnvVar != "" { - var err error - runTests, err = strconv.ParseBool(enableEnvVar) +func waitUntilInstallCompletes(t *testing.T, fluxPluginClient fluxplugin.FluxV2PackagesServiceClient, installedPackageRef *corev1.InstalledPackageReference, expectInstallFailure bool) (actualResp *corev1.GetInstalledPackageDetailResponse) { + const maxWait = 25 + for i := 0; i <= maxWait; i++ { + resp2, err := fluxPluginClient.GetInstalledPackageDetail( + context.TODO(), + &corev1.GetInstalledPackageDetailRequest{InstalledPackageRef: installedPackageRef}) if err != nil { t.Fatalf("%+v", err) } - } - if !runTests { - t.Skipf("skipping flux plugin integration tests as %q not set to be true", envVarFluxIntegrationTests) - } else { - if up, err := isLocalKindClusterUp(t); err != nil || !up { - t.Fatalf("Failed to find local kind cluster due to: [%v]", err) - } - var fluxPluginClient fluxplugin.FluxV2PackagesServiceClient - var err error - if fluxPluginClient, err = getFluxPluginClient(t); err != nil { - t.Fatalf("Failed to get fluxv2 plugin due to: [%v]", err) + if !expectInstallFailure { + if resp2.InstalledPackageDetail.Status.Ready == true && + resp2.InstalledPackageDetail.Status.Reason == corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED { + actualResp = resp2 + break + } + } else { + if resp2.InstalledPackageDetail.Status.Ready == false && + resp2.InstalledPackageDetail.Status.Reason == corev1.InstalledPackageStatus_STATUS_REASON_FAILED { + actualResp = resp2 + break + } } - rand.Seed(time.Now().UnixNano()) - return fluxPluginClient + t.Logf("Waiting 1s due to: [%s], userReason: [%s], attempt: [%d/%d]...", + resp2.InstalledPackageDetail.Status.Reason, resp2.InstalledPackageDetail.Status.UserReason, i+1, maxWait) + time.Sleep(1 * time.Second) } - return nil -} -func isLocalKindClusterUp(t *testing.T) (up bool, err error) { - t.Logf("+isLocalKindClusterUp") - cmd := exec.Command("kind", "get", "clusters") - bytes, err := cmd.CombinedOutput() - if err != nil { - t.Logf("%s", string(bytes)) - return false, err - } - if !strings.Contains(string(bytes), "kubeapps\n") { - return false, nil + if actualResp == nil { + t.Fatalf("Timed out waiting for task to complete") } + return actualResp +} - // naively assume that if the api server reports nodes, the cluster is up - typedClient, err := kubeGetTypedClient() - if err != nil { - t.Logf("%s", string(bytes)) - return false, err +// global vars +var ( + create_request_basic = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-1/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo", + TargetContext: &corev1.Context{ + Namespace: "test-1", + }, } - nodeList, err := typedClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - t.Logf("%s", string(bytes)) - return false, err + expected_detail_basic = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "*", + }, + Name: "my-podinfo", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-1/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, } - if len(nodeList.Items) == 1 || nodeList.Items[0].Name == "node/kubeapps-control-plane" { - return true, nil - } else { - return false, fmt.Errorf("Unexpected cluster nodes: [%v]", nodeList) + create_request_semver_constraint = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-2/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-2", + TargetContext: &corev1.Context{ + Namespace: "test-2", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "> 5", + }, } -} -func getFluxPluginClient(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, error) { - t.Logf("+getFluxPluginClient") - - var opts []grpc.DialOption - opts = append(opts, grpc.WithInsecure()) - opts = append(opts, grpc.WithBlock()) - target := "localhost:8080" - conn, err := grpc.Dial(target, opts...) - if err != nil { - t.Fatalf("failed to dial [%s] due to: %v", target, err) - } - t.Cleanup(func() { conn.Close() }) - pluginsCli := plugins.NewPluginsServiceClient(conn) - response, err := pluginsCli.GetConfiguredPlugins(context.TODO(), &plugins.GetConfiguredPluginsRequest{}) - if err != nil { - t.Fatalf("failed to GetConfiguredPlugins due to: %v", err) - } - found := false - for _, p := range response.Plugins { - if p.Name == "fluxv2.packages" && p.Version == "v1alpha1" { - found = true - break - } + expected_detail_semver_constraint = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-2", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "> 5", + }, + Name: "my-podinfo-2", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-2 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-2/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, } - if !found { - return nil, fmt.Errorf("kubeapps Flux v2 plugin is not registered") + + create_request_reconcile_options = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-3/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-3", + TargetContext: &corev1.Context{ + Namespace: "test-3", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + Suspend: false, + ServiceAccountName: "foo", + }, } - return fluxplugin.NewFluxV2PackagesServiceClient(conn), nil -} -// This should eventually be replaced with fluxPlugin CreateRepository() call as soon as we finalize -// the design -func kubeCreateHelmRepository(t *testing.T, name, url, namespace string) error { - t.Logf("+kubeCreateHelmRepository(%s,%s)", name, namespace) - unstructuredRepo := unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": fmt.Sprintf("%s/%s", fluxGroup, fluxVersion), - "kind": fluxHelmRepository, - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, + expected_detail_reconcile_options = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", }, - "spec": map[string]interface{}{ - "url": url, - "interval": "1m", + Identifier: "my-podinfo-3", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "*", + }, + Name: "my-podinfo-3", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + Suspend: false, + ServiceAccountName: "foo", + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-3 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-3/podinfo", + Context: &corev1.Context{ + Namespace: "default", }, + Plugin: fluxPlugin, }, } - if ifc, err := kubeGetHelmRepositoryResourceInterface(namespace); err != nil { - return err - } else if _, err = ifc.Create(context.TODO(), &unstructuredRepo, metav1.CreateOptions{}); err != nil { - return err + create_request_with_values = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-4/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-4", + TargetContext: &corev1.Context{ + Namespace: "test-4", + }, + Values: "{\"ui\": { \"message\": \"what we do in the shadows\" } }", } - return nil -} -// this should eventually be replaced with flux plugin's DeleteRepository() -func kubeDeleteHelmRepository(t *testing.T, name, namespace string) error { - t.Logf("+kubeDeleteHelmRepository(%s,%s)", name, namespace) - if ifc, err := kubeGetHelmRepositoryResourceInterface(namespace); err != nil { - return err - } else if err = ifc.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { - return err + expected_detail_with_values = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-4", + Plugin: fluxPlugin, + }, + Name: "my-podinfo-4", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "*", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-4 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-4/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", } - return nil -} -// this should eventually be replaced with flux plugin's DeleteInstalledPackage() -func kubeDeleteHelmRelease(t *testing.T, name, namespace string) error { - t.Logf("+kubeDeleteHelmRelease(%s,%s)", name, namespace) - if ifc, err := kubeGetHelmReleaseResourceInterface(namespace); err != nil { - return err - // remove finalizer on HelmRelease cuz sometimes it gets stuck indefinitely - } else if _, err = ifc.Patch(context.TODO(), name, types.JSONPatchType, - []byte("[ { \"op\": \"remove\", \"path\": \"/metadata/finalizers\" } ]"), metav1.PatchOptions{}); err != nil { - return err - } else if err = ifc.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { - return err - } - return nil -} - -func kubeGetPodNames(t *testing.T, namespace string) (names []string, err error) { - t.Logf("+kubeGetPodNames(%s)", namespace) - if typedClient, err := kubeGetTypedClient(); err != nil { - return nil, err - } else if podList, err := typedClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}); err != nil { - return nil, err - } else { - names := []string{} - for _, p := range podList.Items { - names = append(names, p.GetName()) - } - return names, nil + create_request_install_fails = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-5/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-5", + TargetContext: &corev1.Context{ + Namespace: "test-5", + }, + Values: "{\"replicaCount\": \"what we do in the shadows\" }", } -} -// will create a service account with cluster-admin privs -func kubeCreateServiceAccount(t *testing.T, name, namespace string) error { - t.Logf("+kubeCreateServiceAccount(%s,%s)", name, namespace) - if typedClient, err := kubeGetTypedClient(); err != nil { - return err - } else if _, err = typedClient.CoreV1().ServiceAccounts(namespace).Create( - context.TODO(), - &kubecorev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - metav1.CreateOptions{}); err != nil { - return err - } else if _, err = typedClient.RbacV1().ClusterRoleBindings().Create( - context.TODO(), - &kuberbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: name + "-binding", - }, - Subjects: []kuberbacv1.Subject{ - { - Kind: kuberbacv1.ServiceAccountKind, - Name: name, - Namespace: namespace, - }, - }, - RoleRef: kuberbacv1.RoleRef{ - Kind: "ClusterRole", - Name: "cluster-admin", - }, - }, - metav1.CreateOptions{}); err != nil { - return err - } - return nil -} - -func kubeDeleteServiceAccount(t *testing.T, name, namespace string) error { - t.Logf("+kubeDeleteServiceAccount(%s,%s)", name, namespace) - if typedClient, err := kubeGetTypedClient(); err != nil { - return err - } else if err = typedClient.RbacV1().ClusterRoleBindings().Delete( - context.TODO(), - name+"-binding", - metav1.DeleteOptions{}); err != nil { - return err - } else if err = typedClient.CoreV1().ServiceAccounts(namespace).Delete( - context.TODO(), - name, - metav1.DeleteOptions{}); err != nil { - return err - } - return nil -} - -func kubeDeleteNamespace(t *testing.T, namespace string) error { - t.Logf("+kubeDeleteNamespace(%s)", namespace) - if typedClient, err := kubeGetTypedClient(); err != nil { - return err - } else if typedClient.CoreV1().Namespaces().Delete( - context.TODO(), - namespace, - metav1.DeleteOptions{}); err != nil { - return err - } - return nil -} - -func kubeGetHelmReleaseResourceInterface(namespace string) (dynamic.ResourceInterface, error) { - clientset, err := kubeGetDynamicClient() - if err != nil { - return nil, err + expected_detail_install_fails = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-5", + Plugin: fluxPlugin, + }, + Name: "my-podinfo-5", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "*", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, + // most of the time it fails with + // "InstallFailed: install retries exhausted", + // but every once in a while you get + // "InstallFailed: Helm install failed: unable to build kubernetes objects from release manifest: error + // validating "": error validating data: ValidationError(Deployment.spec.replicas): invalid type for + // io.k8s.api.apps.v1.DeploymentSpec.replicas: got "string"" + // so we'll just test the prefix + UserReason: "InstallFailed: ", + }, + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-5/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + ValuesApplied: "{\"replicaCount\":\"what we do in the shadows\"}", } - relResource := schema.GroupVersionResource{ - Group: fluxHelmReleaseGroup, - Version: fluxHelmReleaseVersion, - Resource: fluxHelmReleases, + + create_request_podinfo_5_2_1 = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-6/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-6", + TargetContext: &corev1.Context{ + Namespace: "test-6", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, } - return clientset.Resource(relResource).Namespace(namespace), nil -} -func kubeGetHelmRepositoryResourceInterface(namespace string) (dynamic.ResourceInterface, error) { - clientset, err := kubeGetDynamicClient() - if err != nil { - return nil, err + expected_detail_podinfo_5_2_1 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-6", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-6", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-6 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-6/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, } - repoResource := schema.GroupVersionResource{ - Group: fluxGroup, - Version: fluxVersion, - Resource: fluxHelmRepositories, + + expected_detail_podinfo_6_0_0 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-6", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "6.0.0", + }, + Name: "my-podinfo-6", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-6 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-6/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, } - return clientset.Resource(repoResource).Namespace(namespace), nil -} -func kubeGetDynamicClient() (dynamic.Interface, error) { - if dynamicClient != nil { - return dynamicClient, nil - } else { - if config, err := restConfig(); err != nil { - return nil, err - } else { - dynamicClient, err = dynamic.NewForConfig(config) - return dynamicClient, err - } + create_request_podinfo_5_2_1_no_values = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-7/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-7", + TargetContext: &corev1.Context{ + Namespace: "test-7", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, } -} -func kubeGetTypedClient() (kubernetes.Interface, error) { - if typedClient != nil { - return typedClient, nil - } else { - if config, err := restConfig(); err != nil { - return nil, err - } else { - typedClient, err = kubernetes.NewForConfig(config) - return typedClient, err - } + expected_detail_podinfo_5_2_1_no_values = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-7", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-7", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-7 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-7/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, } -} -func restConfig() (*rest.Config, error) { - kubeconfig := os.Getenv("KUBECONFIG") - return clientcmd.BuildConfigFromFlags("", kubeconfig) -} + expected_detail_podinfo_5_2_1_values = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-7", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-7", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-7 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-7/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -func randSeq(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] + create_request_podinfo_5_2_1_values_2 = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-8/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-8", + TargetContext: &corev1.Context{ + Namespace: "test-8", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", } - return string(b) -} -// global vars -var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") - -var typedClient kubernetes.Interface -var dynamicClient dynamic.Interface - -var create_request_basic = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-1/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo", - TargetContext: &corev1.Context{ - Namespace: "test-1", - }, -} + expected_detail_podinfo_5_2_1_values_2 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-8", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-8", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-8 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-8/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -var expected_detail_basic = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo", - Plugin: fluxPlugin, - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "*", - }, - Name: "my-podinfo", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.0.0", - AppVersion: "6.0.0", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - PostInstallationNotes: "1. Get the application URL by running these commands:\n " + - "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + - "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo 8080:9898\n", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-1/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, -} + expected_detail_podinfo_5_2_1_values_3 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-8", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-8", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + ValuesApplied: "{\"ui\":{\"message\":\"Le Bureau des Légendes\"}}", + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-8 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-8/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -var create_request_semver_constraint = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-2/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo-2", - TargetContext: &corev1.Context{ - Namespace: "test-2", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "> 5", - }, -} + create_request_podinfo_5_2_1_values_4 = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-9/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-9", + TargetContext: &corev1.Context{ + Namespace: "test-9", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + } -var expected_detail_semver_constraint = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo-2", - Plugin: fluxPlugin, - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "> 5", - }, - Name: "my-podinfo-2", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.0.0", - AppVersion: "6.0.0", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - PostInstallationNotes: "1. Get the application URL by running these commands:\n " + - "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + - "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-2 8080:9898\n", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-2/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, -} + expected_detail_podinfo_5_2_1_values_4 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-9", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-9", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-9 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-9/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -var create_request_reconcile_options = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-3/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo-3", - TargetContext: &corev1.Context{ - Namespace: "test-3", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - Suspend: false, - ServiceAccountName: "foo", - }, -} + expected_detail_podinfo_5_2_1_values_5 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-9", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-9", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-9 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-9/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -var expected_detail_reconcile_options = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo-3", - Plugin: fluxPlugin, - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "*", - }, - Name: "my-podinfo-3", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.0.0", - AppVersion: "6.0.0", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - Suspend: false, - ServiceAccountName: "foo", - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - PostInstallationNotes: "1. Get the application URL by running these commands:\n " + - "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + - "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-3 8080:9898\n", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-3/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, -} + create_request_podinfo_5_2_1_values_6 = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-10/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + }, + Name: "my-podinfo-10", + TargetContext: &corev1.Context{ + Namespace: "test-10", + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + } -var create_request_with_values = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-4/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo-4", - TargetContext: &corev1.Context{ - Namespace: "test-4", - }, - Values: "{\"ui\": { \"message\": \"what we do in the shadows\" } }", -} + expected_detail_podinfo_5_2_1_values_6 = &corev1.InstalledPackageDetail{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo-10", + Plugin: fluxPlugin, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Name: "my-podinfo-10", + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "5.2.1", + AppVersion: "5.2.1", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", + Status: statusInstalled, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-10 8080:9898\n", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "podinfo-10/podinfo", + Context: &corev1.Context{ + Namespace: "default", + }, + Plugin: fluxPlugin, + }, + } -var expected_detail_with_values = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo-4", - Plugin: fluxPlugin, - }, - Name: "my-podinfo-4", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.0.0", - AppVersion: "6.0.0", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "*", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - PostInstallationNotes: "1. Get the application URL by running these commands:\n " + - "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + - "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-4 8080:9898\n", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-4/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, - ValuesApplied: "{\"ui\":{\"message\":\"what we do in the shadows\"}}", -} + update_request_1 = &corev1.UpdateInstalledPackageRequest{ + // InstalledPackageRef will be filled in by the code below after a call to create(...) completes + PkgVersionReference: &corev1.VersionReference{ + Version: "6.0.0", + }, + } -var create_request_install_fails = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-5/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo-5", - TargetContext: &corev1.Context{ - Namespace: "test-5", - }, - Values: "{\"replicaCount\": \"what we do in the shadows\" }", -} + update_request_2 = &corev1.UpdateInstalledPackageRequest{ + // InstalledPackageRef will be filled in by the code below after a call to create(...) completes + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\": { \"message\": \"what we do in the shadows\" } }", + } -var expected_detail_install_fails = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo-5", - Plugin: fluxPlugin, - }, - Name: "my-podinfo-5", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.0.0", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "*", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, - UserReason: "InstallFailed: install retries exhausted", - }, - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-5/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, - ValuesApplied: "{\"replicaCount\":\"what we do in the shadows\"}", -} + update_request_3 = &corev1.UpdateInstalledPackageRequest{ + // InstalledPackageRef will be filled in by the code below after a call to create(...) completes + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\": { \"message\": \"Le Bureau des Légendes\" } }", + } -var create_request_podinfo_5_2_1 = &corev1.CreateInstalledPackageRequest{ - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-6/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - }, - Name: "my-podinfo-6", - TargetContext: &corev1.Context{ - Namespace: "test-6", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "=5.2.1", - }, -} + update_request_4 = &corev1.UpdateInstalledPackageRequest{ + // InstalledPackageRef will be filled in by the code below after a call to create(...) completes + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "", + } -var expected_detail_podinfo_5_2_1 = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", - }, - Identifier: "my-podinfo-6", - Plugin: fluxPlugin, - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "=5.2.1", - }, - Name: "my-podinfo-6", - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "5.2.1", - AppVersion: "5.2.1", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - PostInstallationNotes: "1. Get the application URL by running these commands:\n " + - "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + - "kubectl -n @TARGET_NS@ port-forward deploy/@TARGET_NS@-my-podinfo-6 8080:9898\n", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "podinfo-6/podinfo", - Context: &corev1.Context{ - Namespace: "default", - }, - Plugin: fluxPlugin, - }, -} + update_request_5 = &corev1.UpdateInstalledPackageRequest{ + // InstalledPackageRef will be filled in by the code below after a call to create(...) completes + PkgVersionReference: &corev1.VersionReference{ + Version: "=5.2.1", + }, + Values: "{\"ui\": { \"message\": \"what we do in the shadows\" } }", + } +) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go index 02eb2905a37..e12f1e6f404 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go @@ -19,6 +19,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" redismock "github.com/go-redis/redismock/v8" @@ -61,6 +62,7 @@ type testSpecGetInstalledPackages struct { releaseSuspend bool releaseServiceAccountName string releaseStatus map[string]interface{} + targetNamespace string } func TestGetInstalledPackageSummaries(t *testing.T) { @@ -243,76 +245,12 @@ func TestGetInstalledPackageSummaries(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - runtimeObjs := []runtime.Object{} - for _, existing := range tc.existingObjs { - tarGzBytes, err := ioutil.ReadFile(existing.chartTarGz) - if err != nil { - t.Fatalf("%+v", err) - } - - // stand up an http server just for the duration of this test - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write(tarGzBytes) - })) - defer ts.Close() - - chartSpec := map[string]interface{}{ - "chart": existing.chartName, - "sourceRef": map[string]interface{}{ - "name": existing.repoName, - "kind": fluxHelmRepository, - }, - "version": existing.chartSpecVersion, - "interval": "10m", - } - chartStatus := map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "type": "Ready", - "status": "True", - "reason": "ChartPullSucceeded", - }, - }, - "artifact": map[string]interface{}{ - "revision": existing.chartArtifactVersion, - }, - "url": ts.URL, - } - chart := newChart(existing.chartName, existing.repoNamespace, chartSpec, chartStatus) - runtimeObjs = append(runtimeObjs, chart) - - releaseSpec := map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": existing.chartName, - "version": existing.chartSpecVersion, - "sourceRef": map[string]interface{}{ - "name": existing.repoName, - "kind": fluxHelmRepository, - "namespace": existing.repoNamespace, - }, - }, - }, - "interval": "1m", - } - if len(existing.releaseValues) != 0 { - unstructured.SetNestedMap(releaseSpec, existing.releaseValues, "values") - } - if existing.releaseSuspend { - unstructured.SetNestedField(releaseSpec, existing.releaseSuspend, "suspend") - } - if len(existing.releaseServiceAccountName) != 0 { - unstructured.SetNestedField(releaseSpec, existing.releaseServiceAccountName, "serviceAccountName") - } - release := newRelease(existing.releaseName, existing.releaseNamespace, releaseSpec, existing.releaseStatus) - runtimeObjs = append(runtimeObjs, release) - } - + runtimeObjs, cleanup := newRuntimeObjects(t, tc.existingObjs) s, mock, _, err := newServerWithChartsAndReleases(nil, runtimeObjs...) if err != nil { t.Fatalf("%+v", err) } + defer cleanup() for i, existing := range tc.existingObjs { if tc.request.GetPaginationOptions().GetPageSize() > 0 { @@ -390,73 +328,49 @@ func TestGetInstalledPackageDetail(t *testing.T) { targetNamespace string // this is where installation would actually place artifacts existingHelmStubs []helmReleaseStub expectedStatusCode codes.Code - expectedResponse *corev1.GetInstalledPackageDetailResponse + expectedDetail *corev1.InstalledPackageDetail }{ { name: "returns installed package detail when install fails", request: &corev1.GetInstalledPackageDetailRequest{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Identifier: "my-redis", - Context: &corev1.Context{ - Namespace: "namespace-1", - }, - }, + InstalledPackageRef: my_redis_ref, }, existingK8sObjs: []testSpecGetInstalledPackages{ redis_existing_spec_failed, }, - targetNamespace: "test", existingHelmStubs: []helmReleaseStub{ redis_existing_stub_failed, }, expectedStatusCode: codes.OK, - expectedResponse: &corev1.GetInstalledPackageDetailResponse{ - InstalledPackageDetail: redis_detail_failed, - }, + expectedDetail: redis_detail_failed, }, { name: "returns installed package detail when install is in progress", request: &corev1.GetInstalledPackageDetailRequest{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Identifier: "my-redis", - Context: &corev1.Context{ - Namespace: "namespace-1", - }, - }, + InstalledPackageRef: my_redis_ref, }, existingK8sObjs: []testSpecGetInstalledPackages{ redis_existing_spec_pending, }, - targetNamespace: "test", existingHelmStubs: []helmReleaseStub{ redis_existing_stub_pending, }, expectedStatusCode: codes.OK, - expectedResponse: &corev1.GetInstalledPackageDetailResponse{ - InstalledPackageDetail: redis_detail_pending, - }, + expectedDetail: redis_detail_pending, }, { name: "returns installed package detail when install is successful", request: &corev1.GetInstalledPackageDetailRequest{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Identifier: "my-redis", - Context: &corev1.Context{ - Namespace: "namespace-1", - }, - }, + InstalledPackageRef: my_redis_ref, }, existingK8sObjs: []testSpecGetInstalledPackages{ redis_existing_spec_completed, }, - targetNamespace: "test", existingHelmStubs: []helmReleaseStub{ redis_existing_stub_completed, }, expectedStatusCode: codes.OK, - expectedResponse: &corev1.GetInstalledPackageDetailResponse{ - InstalledPackageDetail: redis_detail_completed, - }, + expectedDetail: redis_detail_completed, }, { name: "returns a 404 if the installed package is not found", @@ -471,104 +385,28 @@ func TestGetInstalledPackageDetail(t *testing.T) { existingK8sObjs: []testSpecGetInstalledPackages{ redis_existing_spec_completed, }, - targetNamespace: "test", expectedStatusCode: codes.NotFound, }, { name: "returns values and reconciliation options in package detail", request: &corev1.GetInstalledPackageDetailRequest{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", - }, - Identifier: "my-redis", - }, + InstalledPackageRef: my_redis_ref, }, existingK8sObjs: []testSpecGetInstalledPackages{ redis_existing_spec_completed_with_values_and_reconciliation_options, }, - targetNamespace: "test", existingHelmStubs: []helmReleaseStub{ redis_existing_stub_completed, }, expectedStatusCode: codes.OK, - expectedResponse: &corev1.GetInstalledPackageDetailResponse{ - InstalledPackageDetail: redis_detail_completed_with_values_and_reconciliation_options, - }, + expectedDetail: redis_detail_completed_with_values_and_reconciliation_options, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - runtimeObjs := []runtime.Object{} - for _, existing := range tc.existingK8sObjs { - tarGzBytes, err := ioutil.ReadFile(existing.chartTarGz) - if err != nil { - t.Fatalf("%+v", err) - } - - // stand up an http server just for the duration of this test - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write(tarGzBytes) - })) - defer ts.Close() - - chartSpec := map[string]interface{}{ - "chart": existing.chartName, - "sourceRef": map[string]interface{}{ - "name": existing.repoName, - "kind": fluxHelmRepository, - }, - "version": existing.chartSpecVersion, - "interval": "1m", - } - chartStatus := map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-12T03:25:38Z", - "message": "Fetched revision: " + existing.chartSpecVersion, - "type": "Ready", - "status": "True", - "reason": "ChartPullSucceeded", - }, - }, - "artifact": map[string]interface{}{ - "revision": existing.chartArtifactVersion, - }, - "url": ts.URL, - } - chart := newChart(existing.chartName, existing.repoNamespace, chartSpec, chartStatus) - runtimeObjs = append(runtimeObjs, chart) - - releaseSpec := map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": existing.chartName, - "version": existing.chartSpecVersion, - "sourceRef": map[string]interface{}{ - "name": existing.repoName, - "kind": fluxHelmRepository, - "namespace": existing.repoNamespace, - }, - }, - }, - "interval": "1m", - "targetNamespace": tc.targetNamespace, - } - if len(existing.releaseValues) != 0 { - unstructured.SetNestedMap(releaseSpec, existing.releaseValues, "values") - } - if existing.releaseSuspend { - unstructured.SetNestedField(releaseSpec, existing.releaseSuspend, "suspend") - } - if len(existing.releaseServiceAccountName) != 0 { - unstructured.SetNestedField(releaseSpec, existing.releaseServiceAccountName, "serviceAccountName") - } - release := newRelease(existing.releaseName, existing.releaseNamespace, releaseSpec, existing.releaseStatus) - runtimeObjs = append(runtimeObjs, release) - } - + runtimeObjs, cleanup := newRuntimeObjects(t, tc.existingK8sObjs) + defer cleanup() actionConfig := newHelmActionConfig(t, tc.targetNamespace, tc.existingHelmStubs) s, mock, _, err := newServerWithChartsAndReleases(actionConfig, runtimeObjs...) if err != nil { @@ -586,21 +424,12 @@ func TestGetInstalledPackageDetail(t *testing.T) { return } - opts := cmpopts.IgnoreUnexported( - corev1.GetInstalledPackageDetailResponse{}, - corev1.InstalledPackageDetail{}, - corev1.InstalledPackageReference{}, - corev1.Context{}, - corev1.VersionReference{}, - corev1.InstalledPackageStatus{}, - corev1.PackageAppVersion{}, - plugins.Plugin{}, - corev1.ReconciliationOptions{}, - corev1.AvailablePackageReference{}) - if got, want := response, tc.expectedResponse; !cmp.Equal(want, got, opts) { - t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts)) + expectedResp := &corev1.GetInstalledPackageDetailResponse{ + InstalledPackageDetail: tc.expectedDetail, } + compareActualVsExpectedGetInstalledPackageDetailResponse(t, response, expectedResp) + // we make sure that all expectations were met if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) @@ -800,6 +629,208 @@ func TestCreateInstalledPackage(t *testing.T) { } } +func TestUpdateInstalledPackage(t *testing.T) { + testCases := []struct { + name string + request *corev1.UpdateInstalledPackageRequest + existingK8sObjs []testSpecGetInstalledPackages + expectedStatusCode codes.Code + expectedResponse *corev1.UpdateInstalledPackageResponse + expectedRelease map[string]interface{} + }{ + { + name: "update package (simple)", + request: &corev1.UpdateInstalledPackageRequest{ + InstalledPackageRef: my_redis_ref, + PkgVersionReference: &corev1.VersionReference{ + Version: ">14.4.0", + }, + }, + existingK8sObjs: []testSpecGetInstalledPackages{ + redis_existing_spec_completed, + }, + expectedStatusCode: codes.OK, + expectedResponse: &corev1.UpdateInstalledPackageResponse{ + InstalledPackageRef: my_redis_ref, + }, + expectedRelease: flux_helm_release_updated_1, + }, + { + name: "returns not found if installed package doesn't exist", + request: &corev1.UpdateInstalledPackageRequest{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "default", + }, + Identifier: "not-a-valid-identifier", + }, + }, + expectedStatusCode: codes.NotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + runtimeObjs, cleanup := newRuntimeObjects(t, tc.existingK8sObjs) + defer cleanup() + s, mock, _, err := newServerWithChartsAndReleases(nil, runtimeObjs...) + if err != nil { + t.Fatalf("%+v", err) + } + + response, err := s.UpdateInstalledPackage(context.Background(), tc.request) + + if got, want := status.Code(err), tc.expectedStatusCode; got != want { + t.Fatalf("got: %+v, want: %+v, err: %+v", got, want, err) + } + + // We don't need to check anything else for non-OK codes. + if tc.expectedStatusCode != codes.OK { + return + } + + opts := cmpopts.IgnoreUnexported( + corev1.UpdateInstalledPackageResponse{}, + corev1.InstalledPackageReference{}, + plugins.Plugin{}, + corev1.Context{}) + + if got, want := response, tc.expectedResponse; !cmp.Equal(want, got, opts) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts)) + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + // check expected HelmReleass CRD has been updated + dynamicClient, _, err = s.clientGetter(context.Background()) + if err != nil { + t.Fatalf("%+v", err) + } + + releaseObj, err := dynamicClient.Resource(releasesGvr). + Namespace(tc.expectedResponse.InstalledPackageRef.Context.Namespace).Get( + context.Background(), + tc.expectedResponse.InstalledPackageRef.Identifier, + v1.GetOptions{}) + if err != nil { + t.Fatalf("%+v", err) + } + + if got, want := releaseObj.Object, tc.expectedRelease; !cmp.Equal(want, got) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got)) + } + }) + } +} + +func newRuntimeObjects(t *testing.T, existingK8sObjs []testSpecGetInstalledPackages) (runtimeObjs []runtime.Object, cleanup func()) { + httpServers := []*httptest.Server{} + cleanup = func() { + for _, ts := range httpServers { + ts.Close() + } + } + + for _, existing := range existingK8sObjs { + tarGzBytes, err := ioutil.ReadFile(existing.chartTarGz) + if err != nil { + t.Fatalf("%+v", err) + } + + // stand up an http server just for the duration of this test + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(tarGzBytes) + })) + httpServers = append(httpServers, ts) + + chartSpec := map[string]interface{}{ + "chart": existing.chartName, + "sourceRef": map[string]interface{}{ + "name": existing.repoName, + "kind": fluxHelmRepository, + }, + "version": existing.chartSpecVersion, + "interval": "1m", + } + chartStatus := map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-12T03:25:38Z", + "message": "Fetched revision: " + existing.chartSpecVersion, + "type": "Ready", + "status": "True", + "reason": "ChartPullSucceeded", + }, + }, + "artifact": map[string]interface{}{ + "revision": existing.chartArtifactVersion, + }, + "url": ts.URL, + } + chart := newChart(existing.chartName, existing.repoNamespace, chartSpec, chartStatus) + runtimeObjs = append(runtimeObjs, chart) + + releaseSpec := map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": existing.chartName, + "version": existing.chartSpecVersion, + "sourceRef": map[string]interface{}{ + "name": existing.repoName, + "kind": fluxHelmRepository, + "namespace": existing.repoNamespace, + }, + }, + }, + "interval": "1m", + "install": map[string]interface{}{ + "createNamespace": true, + }, + } + if len(existing.targetNamespace) != 0 { + unstructured.SetNestedField(releaseSpec, existing.targetNamespace, "targetNamespace") + } + if len(existing.releaseValues) != 0 { + unstructured.SetNestedMap(releaseSpec, existing.releaseValues, "values") + } + if existing.releaseSuspend { + unstructured.SetNestedField(releaseSpec, existing.releaseSuspend, "suspend") + } + if len(existing.releaseServiceAccountName) != 0 { + unstructured.SetNestedField(releaseSpec, existing.releaseServiceAccountName, "serviceAccountName") + } + release := newRelease(existing.releaseName, existing.releaseNamespace, releaseSpec, existing.releaseStatus) + runtimeObjs = append(runtimeObjs, release) + } + return runtimeObjs, cleanup +} + +func compareActualVsExpectedGetInstalledPackageDetailResponse(t *testing.T, actualResp *corev1.GetInstalledPackageDetailResponse, expectedResp *corev1.GetInstalledPackageDetailResponse) { + opts := cmpopts.IgnoreUnexported( + corev1.GetInstalledPackageDetailResponse{}, + corev1.InstalledPackageDetail{}, + corev1.InstalledPackageReference{}, + corev1.Context{}, + corev1.VersionReference{}, + corev1.InstalledPackageStatus{}, + corev1.PackageAppVersion{}, + plugins.Plugin{}, + corev1.ReconciliationOptions{}, + corev1.AvailablePackageReference{}) + // see comment in release_intergration_test.go. Intermittently we get an inconsistent error message from flux + opts2 := cmpopts.IgnoreFields(corev1.InstalledPackageStatus{}, "UserReason") + if got, want := actualResp, expectedResp; !cmp.Equal(want, got, opts, opts2) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts, opts2)) + } + if !strings.Contains(actualResp.InstalledPackageDetail.Status.UserReason, expectedResp.InstalledPackageDetail.Status.UserReason) { + t.Errorf("substring mismatch (-want: %s\n+got: %s):\n", expectedResp.InstalledPackageDetail.Status.UserReason, actualResp.InstalledPackageDetail.Status.UserReason) + } +} + func newRelease(name string, namespace string, spec map[string]interface{}, status map[string]interface{}) *unstructured.Unstructured { metadata := map[string]interface{}{ "name": name, @@ -918,745 +949,716 @@ func newHelmActionConfig(t *testing.T, namespace string, rels []helmReleaseStub) } // misc global vars that get re-used in multiple tests scenarios -var releasesGvr = schema.GroupVersionResource{ - Group: fluxHelmReleaseGroup, - Version: fluxHelmReleaseVersion, - Resource: fluxHelmReleases, -} +var ( + releasesGvr = schema.GroupVersionResource{ + Group: fluxHelmReleaseGroup, + Version: fluxHelmReleaseVersion, + Resource: fluxHelmReleases, + } -var redis_summary_installed = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", - }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - PkgDisplayName: "redis", - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Status: &corev1.InstalledPackageStatus{ + statusInstalled = &corev1.InstalledPackageStatus{ Ready: true, Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.6.1", - AppVersion: "6.2.4", - }, -} + } -var redis_summary_failed = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ + my_redis_ref = &corev1.InstalledPackageReference{ Context: &corev1.Context{ Namespace: "namespace-1", }, Identifier: "my-redis", Plugin: fluxPlugin, - }, - Name: "my-redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - PkgDisplayName: "redis", - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, - UserReason: "InstallFailed: install retries exhausted", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.6.1", - AppVersion: "6.2.4", - }, -} + } -var redis_summary_pending = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_summary_installed = &corev1.InstalledPackageSummary{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - PkgDisplayName: "redis", - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, - UserReason: "Progressing: reconciliation in progress", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.6.1", - AppVersion: "6.2.4", - }, -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", + }, + PkgDisplayName: "redis", + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Status: statusInstalled, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.6.1", + AppVersion: "6.2.4", + }, + } -var redis_summary_pending_2 = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_summary_failed = &corev1.InstalledPackageSummary{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - PkgDisplayName: "redis", - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, - UserReason: "ArtifactFailed: HelmChart 'default/kubeapps-my-redis' is not ready", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.6.1", - AppVersion: "6.2.4", - }, -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", + }, + PkgDisplayName: "redis", + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, + UserReason: "InstallFailed: install retries exhausted", + }, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.6.1", + AppVersion: "6.2.4", + }, + } -var airflow_summary_installed = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-2", + redis_summary_pending = &corev1.InstalledPackageSummary{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-airflow", - Plugin: fluxPlugin, - }, - Name: "my-airflow", - IconUrl: "https://bitnami.com/assets/stacks/airflow/img/airflow-stack-110x117.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "6.7.1", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.7.1", - AppVersion: "1.10.12", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "10.2.1", - AppVersion: "2.1.0", - }, - ShortDescription: "Apache Airflow is a platform to programmatically author, schedule and monitor workflows.", - PkgDisplayName: "airflow", - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", + }, + PkgDisplayName: "redis", + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, + UserReason: "Progressing: reconciliation in progress", + }, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.6.1", + AppVersion: "6.2.4", + }, + } -var redis_summary_latest = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_summary_pending_2 = &corev1.InstalledPackageSummary{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "*", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - PkgDisplayName: "redis", - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.6.1", - AppVersion: "6.2.4", - }, -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", + }, + PkgDisplayName: "redis", + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, + UserReason: "ArtifactFailed: HelmChart 'default/kubeapps-my-redis' is not ready", + }, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.6.1", + AppVersion: "6.2.4", + }, + } -var airflow_summary_semver = &corev1.InstalledPackageSummary{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-2", + airflow_summary_installed = &corev1.InstalledPackageSummary{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "namespace-2", + }, + Identifier: "my-airflow", + Plugin: fluxPlugin, }, - Identifier: "my-airflow", - Plugin: fluxPlugin, - }, - Name: "my-airflow", - IconUrl: "https://bitnami.com/assets/stacks/airflow/img/airflow-stack-110x117.png", - PkgVersionReference: &corev1.VersionReference{ - Version: "<=6.7.1", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "6.7.1", - AppVersion: "1.10.12", - }, - LatestVersion: &corev1.PackageAppVersion{ - PkgVersion: "10.2.1", - AppVersion: "2.1.0", - }, - ShortDescription: "Apache Airflow is a platform to programmatically author, schedule and monitor workflows.", - PkgDisplayName: "airflow", - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, -} + Name: "my-airflow", + IconUrl: "https://bitnami.com/assets/stacks/airflow/img/airflow-stack-110x117.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "6.7.1", + }, + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.7.1", + AppVersion: "1.10.12", + }, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "10.2.1", + AppVersion: "2.1.0", + }, + ShortDescription: "Apache Airflow is a platform to programmatically author, schedule and monitor workflows.", + PkgDisplayName: "airflow", + Status: statusInstalled, + } -var redis_existing_spec_completed = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "14.4.0", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "True", - "reason": "ReconciliationSucceeded", - "message": "Release reconciliation succeeded", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Released", - "status": "True", - "reason": "InstallSucceeded", - "message": "Helm install succeeded", - }, - }, - "lastAppliedRevision": "14.4.0", - "lastAttemptedRevision": "14.4.0", - }, -} + redis_summary_latest = &corev1.InstalledPackageSummary{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "*", + }, + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", + }, + PkgDisplayName: "redis", + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Status: statusInstalled, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.6.1", + AppVersion: "6.2.4", + }, + } -var redis_existing_stub_completed = helmReleaseStub{ - name: "test-my-redis", - namespace: "test", - chartVersion: "14.4.0", - notes: "some notes", - status: release.StatusDeployed, -} + airflow_summary_semver = &corev1.InstalledPackageSummary{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "namespace-2", + }, + Identifier: "my-airflow", + Plugin: fluxPlugin, + }, + Name: "my-airflow", + IconUrl: "https://bitnami.com/assets/stacks/airflow/img/airflow-stack-110x117.png", + PkgVersionReference: &corev1.VersionReference{ + Version: "<=6.7.1", + }, + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.7.1", + AppVersion: "1.10.12", + }, + LatestVersion: &corev1.PackageAppVersion{ + PkgVersion: "10.2.1", + AppVersion: "2.1.0", + }, + ShortDescription: "Apache Airflow is a platform to programmatically author, schedule and monitor workflows.", + PkgDisplayName: "airflow", + Status: statusInstalled, + } -var redis_existing_spec_completed_with_values_and_reconciliation_options = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "14.4.0", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseSuspend: true, - releaseServiceAccountName: "foo", - releaseValues: map[string]interface{}{ - "replica": []interface{}{ - map[string]interface{}{ - "replicaCount": "1", - "configuration": "xyz", - }, - }, - }, - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "True", - "reason": "ReconciliationSucceeded", - "message": "Release reconciliation succeeded", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Released", - "status": "True", - "reason": "InstallSucceeded", - "message": "Helm install succeeded", - }, - }, - "lastAppliedRevision": "14.4.0", - "lastAttemptedRevision": "14.4.0", - }, -} + redis_existing_spec_completed = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "14.4.0", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "True", + "reason": "ReconciliationSucceeded", + "message": "Release reconciliation succeeded", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Released", + "status": "True", + "reason": "InstallSucceeded", + "message": "Helm install succeeded", + }, + }, + "lastAppliedRevision": "14.4.0", + "lastAttemptedRevision": "14.4.0", + }, + targetNamespace: "test", + } -var redis_existing_spec_failed = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "14.4.0", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-09-06T10:24:34Z", - "type": "Ready", - "status": "False", - "message": "install retries exhausted", - "reason": "InstallFailed", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-09-06T10:24:34Z", - "type": "Released", - "status": "False", - "message": "Helm install failed: unable to build kubernetes objects from release manifest: error validating \"\": error validating data: ValidationError(Deployment.spec.replicas): invalid type for io.k8s.api.apps.v1.DeploymentSpec.replicas: got \"string\", expected \"integer\"", - "reason": "InstallFailed", - }, - }, - "failures": "14", - "installFailures": "1", - "lastAttemptedRevision": "14.4.0", - }, -} + redis_existing_stub_completed = helmReleaseStub{ + name: "test-my-redis", + namespace: "test", + chartVersion: "14.4.0", + notes: "some notes", + status: release.StatusDeployed, + } -var redis_existing_stub_failed = helmReleaseStub{ - name: "test-my-redis", - namespace: "test", - chartVersion: "14.4.0", - notes: "some notes", - status: release.StatusFailed, -} + redis_existing_spec_completed_with_values_and_reconciliation_options = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "14.4.0", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseSuspend: true, + releaseServiceAccountName: "foo", + releaseValues: map[string]interface{}{ + "replica": []interface{}{ + map[string]interface{}{ + "replicaCount": "1", + "configuration": "xyz", + }, + }, + }, + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "True", + "reason": "ReconciliationSucceeded", + "message": "Release reconciliation succeeded", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Released", + "status": "True", + "reason": "InstallSucceeded", + "message": "Helm install succeeded", + }, + }, + "lastAppliedRevision": "14.4.0", + "lastAttemptedRevision": "14.4.0", + }, + targetNamespace: "test", + } -var airflow_existing_spec_completed = testSpecGetInstalledPackages{ - repoName: "bitnami-2", - repoNamespace: "default", - repoIndex: "testdata/airflow-many-versions.yaml", - chartName: "airflow", - chartTarGz: "testdata/airflow-6.7.1.tgz", - chartSpecVersion: "6.7.1", - chartArtifactVersion: "6.7.1", - releaseName: "my-airflow", - releaseNamespace: "namespace-2", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "True", - "reason": "ReconciliationSucceeded", - "message": "Release reconciliation succeeded", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Released", - "status": "True", - "reason": "InstallSucceeded", - "message": "Helm install succeeded", - }, - }, - "lastAppliedRevision": "6.7.1", - "lastAttemptedRevision": "6.7.1", - }, -} + redis_existing_spec_failed = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "14.4.0", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-09-06T10:24:34Z", + "type": "Ready", + "status": "False", + "message": "install retries exhausted", + "reason": "InstallFailed", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-09-06T10:24:34Z", + "type": "Released", + "status": "False", + "message": "Helm install failed: unable to build kubernetes objects from release manifest: error validating \"\": error validating data: ValidationError(Deployment.spec.replicas): invalid type for io.k8s.api.apps.v1.DeploymentSpec.replicas: got \"string\", expected \"integer\"", + "reason": "InstallFailed", + }, + }, + "failures": "14", + "installFailures": "1", + "lastAttemptedRevision": "14.4.0", + }, + targetNamespace: "test", + } -var airflow_existing_spec_semver = testSpecGetInstalledPackages{ - repoName: "bitnami-2", - repoNamespace: "default", - repoIndex: "testdata/airflow-many-versions.yaml", - chartName: "airflow", - chartTarGz: "testdata/airflow-6.7.1.tgz", - chartSpecVersion: "<=6.7.1", - chartArtifactVersion: "6.7.1", - releaseName: "my-airflow", - releaseNamespace: "namespace-2", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "True", - "reason": "ReconciliationSucceeded", - "message": "Release reconciliation succeeded", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Released", - "status": "True", - "reason": "InstallSucceeded", - "message": "Helm install succeeded", - }, - }, - "lastAppliedRevision": "6.7.1", - "lastAttemptedRevision": "6.7.1", - }, -} + redis_existing_stub_failed = helmReleaseStub{ + name: "test-my-redis", + namespace: "test", + chartVersion: "14.4.0", + notes: "some notes", + status: release.StatusFailed, + } -var redis_existing_spec_pending = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "14.4.0", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "Unknown", - "reason": "Progressing", - "message": "reconciliation in progress", - }, - }, - "lastAttemptedRevision": "14.4.0", - }, -} + airflow_existing_spec_completed = testSpecGetInstalledPackages{ + repoName: "bitnami-2", + repoNamespace: "default", + repoIndex: "testdata/airflow-many-versions.yaml", + chartName: "airflow", + chartTarGz: "testdata/airflow-6.7.1.tgz", + chartSpecVersion: "6.7.1", + chartArtifactVersion: "6.7.1", + releaseName: "my-airflow", + releaseNamespace: "namespace-2", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "True", + "reason": "ReconciliationSucceeded", + "message": "Release reconciliation succeeded", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Released", + "status": "True", + "reason": "InstallSucceeded", + "message": "Helm install succeeded", + }, + }, + "lastAppliedRevision": "6.7.1", + "lastAttemptedRevision": "6.7.1", + }, + } -var redis_existing_spec_pending_2 = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "14.4.0", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-09-06T05:26:52Z", - "message": "HelmChart 'default/kubeapps-my-redis' is not ready", - "reason": "ArtifactFailed", - "status": "False", - "type": "Ready", - }, - }, - "failures": "2", - "lastAttemptedRevision": "14.4.0", - }, -} + airflow_existing_spec_semver = testSpecGetInstalledPackages{ + repoName: "bitnami-2", + repoNamespace: "default", + repoIndex: "testdata/airflow-many-versions.yaml", + chartName: "airflow", + chartTarGz: "testdata/airflow-6.7.1.tgz", + chartSpecVersion: "<=6.7.1", + chartArtifactVersion: "6.7.1", + releaseName: "my-airflow", + releaseNamespace: "namespace-2", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "True", + "reason": "ReconciliationSucceeded", + "message": "Release reconciliation succeeded", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Released", + "status": "True", + "reason": "InstallSucceeded", + "message": "Helm install succeeded", + }, + }, + "lastAppliedRevision": "6.7.1", + "lastAttemptedRevision": "6.7.1", + }, + } -var redis_existing_stub_pending = helmReleaseStub{ - name: "test-my-redis", - namespace: "test", - chartVersion: "14.4.0", - notes: "some notes", - status: release.StatusPendingInstall, -} + redis_existing_spec_pending = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "14.4.0", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "Unknown", + "reason": "Progressing", + "message": "reconciliation in progress", + }, + }, + "lastAttemptedRevision": "14.4.0", + }, + targetNamespace: "test", + } -var redis_existing_spec_latest = testSpecGetInstalledPackages{ - repoName: "bitnami-1", - repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", - chartName: "redis", - chartTarGz: "testdata/redis-14.4.0.tgz", - chartSpecVersion: "*", - chartArtifactVersion: "14.4.0", - releaseName: "my-redis", - releaseNamespace: "namespace-1", - releaseStatus: map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Ready", - "status": "True", - "reason": "ReconciliationSucceeded", - "message": "Release reconciliation succeeded", - }, - map[string]interface{}{ - "lastTransitionTime": "2021-08-11T08:46:03Z", - "type": "Released", - "status": "True", - "reason": "InstallSucceeded", - "message": "Helm install succeeded", - }, - }, - "lastAppliedRevision": "14.4.0", - "lastAttemptedRevision": "14.4.0", - }, -} + redis_existing_spec_pending_2 = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "14.4.0", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-09-06T05:26:52Z", + "message": "HelmChart 'default/kubeapps-my-redis' is not ready", + "reason": "ArtifactFailed", + "status": "False", + "type": "Ready", + }, + }, + "failures": "2", + "lastAttemptedRevision": "14.4.0", + }, + } -var redis_detail_failed = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_existing_stub_pending = helmReleaseStub{ + name: "test-my-redis", + namespace: "test", + chartVersion: "14.4.0", + notes: "some notes", + status: release.StatusPendingInstall, + } + + redis_existing_spec_latest = testSpecGetInstalledPackages{ + repoName: "bitnami-1", + repoNamespace: "default", + repoIndex: "testdata/redis-many-versions.yaml", + chartName: "redis", + chartTarGz: "testdata/redis-14.4.0.tgz", + chartSpecVersion: "*", + chartArtifactVersion: "14.4.0", + releaseName: "my-redis", + releaseNamespace: "namespace-1", + releaseStatus: map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Ready", + "status": "True", + "reason": "ReconciliationSucceeded", + "message": "Release reconciliation succeeded", + }, + map[string]interface{}{ + "lastTransitionTime": "2021-08-11T08:46:03Z", + "type": "Released", + "status": "True", + "reason": "InstallSucceeded", + "message": "Helm install succeeded", + }, + }, + "lastAppliedRevision": "14.4.0", + "lastAttemptedRevision": "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "1.2.3", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, - UserReason: "InstallFailed: install retries exhausted", - }, - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "bitnami-1/redis", - Context: &corev1.Context{Namespace: "default"}, - Plugin: fluxPlugin, - }, - PostInstallationNotes: "some notes", -} + } -var redis_detail_pending = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_detail_failed = &corev1.InstalledPackageDetail{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - CurrentVersion: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "1.2.3", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: false, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, - UserReason: "Progressing: reconciliation in progress", - }, - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "bitnami-1/redis", - Context: &corev1.Context{Namespace: "default"}, - Plugin: fluxPlugin, - }, - PostInstallationNotes: "some notes", -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "1.2.3", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_FAILED, + UserReason: "InstallFailed: install retries exhausted", + }, + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "bitnami-1/redis", + Context: &corev1.Context{Namespace: "default"}, + Plugin: fluxPlugin, + }, + PostInstallationNotes: "some notes", + } -var redis_detail_completed = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_detail_pending = &corev1.InstalledPackageDetail{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - CurrentVersion: &corev1.PackageAppVersion{ - AppVersion: "1.2.3", - PkgVersion: "14.4.0", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "bitnami-1/redis", - Context: &corev1.Context{Namespace: "default"}, - Plugin: fluxPlugin, - }, - PostInstallationNotes: "some notes", -} + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "1.2.3", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: &corev1.InstalledPackageStatus{ + Ready: false, + Reason: corev1.InstalledPackageStatus_STATUS_REASON_PENDING, + UserReason: "Progressing: reconciliation in progress", + }, + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "bitnami-1/redis", + Context: &corev1.Context{Namespace: "default"}, + Plugin: fluxPlugin, + }, + PostInstallationNotes: "some notes", + } -var redis_detail_completed_with_values_and_reconciliation_options = &corev1.InstalledPackageDetail{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "namespace-1", + redis_detail_completed = &corev1.InstalledPackageDetail{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + CurrentVersion: &corev1.PackageAppVersion{ + AppVersion: "1.2.3", + PkgVersion: "14.4.0", }, - Identifier: "my-redis", - Plugin: fluxPlugin, - }, - Name: "my-redis", - CurrentVersion: &corev1.PackageAppVersion{ - AppVersion: "1.2.3", - PkgVersion: "14.4.0", - }, - PkgVersionReference: &corev1.VersionReference{ - Version: "14.4.0", - }, - ReconciliationOptions: &corev1.ReconciliationOptions{ - Interval: 60, - Suspend: true, - ServiceAccountName: "foo", - }, - Status: &corev1.InstalledPackageStatus{ - Ready: true, - Reason: corev1.InstalledPackageStatus_STATUS_REASON_INSTALLED, - UserReason: "ReconciliationSucceeded: Release reconciliation succeeded", - }, - ValuesApplied: "{\"replica\":[{\"configuration\":\"xyz\",\"replicaCount\":\"1\"}]}", - AvailablePackageRef: &corev1.AvailablePackageReference{ - Identifier: "bitnami-1/redis", - Context: &corev1.Context{Namespace: "default"}, - Plugin: fluxPlugin, - }, - PostInstallationNotes: "some notes", -} + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + }, + Status: statusInstalled, + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "bitnami-1/redis", + Context: &corev1.Context{Namespace: "default"}, + Plugin: fluxPlugin, + }, + PostInstallationNotes: "some notes", + } -var flux_helm_release_basic = map[string]interface{}{ - "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", - "kind": "HelmRelease", - "metadata": map[string]interface{}{ - "name": "my-podinfo", - "namespace": "kubeapps", - }, - "spec": map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": "podinfo", - "sourceRef": map[string]interface{}{ - "kind": "HelmRepository", - "name": "podinfo", - "namespace": "namespace-1", - }, - }, + redis_detail_completed_with_values_and_reconciliation_options = &corev1.InstalledPackageDetail{ + InstalledPackageRef: my_redis_ref, + Name: "my-redis", + CurrentVersion: &corev1.PackageAppVersion{ + AppVersion: "1.2.3", + PkgVersion: "14.4.0", }, - "install": map[string]interface{}{ - "createNamespace": true, + PkgVersionReference: &corev1.VersionReference{ + Version: "14.4.0", }, - "interval": "1m", - "targetNamespace": "test", - }, -} + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 60, + Suspend: true, + ServiceAccountName: "foo", + }, + Status: statusInstalled, + ValuesApplied: "{\"replica\":[{\"configuration\":\"xyz\",\"replicaCount\":\"1\"}]}", + AvailablePackageRef: &corev1.AvailablePackageReference{ + Identifier: "bitnami-1/redis", + Context: &corev1.Context{Namespace: "default"}, + Plugin: fluxPlugin, + }, + PostInstallationNotes: "some notes", + } -var flux_helm_release_semver_constraint = map[string]interface{}{ - "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", - "kind": "HelmRelease", - "metadata": map[string]interface{}{ - "name": "my-podinfo", - "namespace": "kubeapps", - }, - "spec": map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": "podinfo", - "sourceRef": map[string]interface{}{ - "kind": "HelmRepository", - "name": "podinfo", - "namespace": "namespace-1", + flux_helm_release_basic = map[string]interface{}{ + "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", + "kind": "HelmRelease", + "metadata": map[string]interface{}{ + "name": "my-podinfo", + "namespace": "kubeapps", + }, + "spec": map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "sourceRef": map[string]interface{}{ + "kind": "HelmRepository", + "name": "podinfo", + "namespace": "namespace-1", + }, }, - "version": "> 5", }, + "install": map[string]interface{}{ + "createNamespace": true, + }, + "interval": "1m", + "targetNamespace": "test", }, - "install": map[string]interface{}{ - "createNamespace": true, - }, - "interval": "1m", - "targetNamespace": "test", - }, -} + } -var flux_helm_release_reconcile_options = map[string]interface{}{ - "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", - "kind": "HelmRelease", - "metadata": map[string]interface{}{ - "name": "my-podinfo", - "namespace": "kubeapps", - }, - "spec": map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": "podinfo", - "sourceRef": map[string]interface{}{ - "kind": "HelmRepository", - "name": "podinfo", - "namespace": "namespace-1", + flux_helm_release_semver_constraint = map[string]interface{}{ + "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", + "kind": "HelmRelease", + "metadata": map[string]interface{}{ + "name": "my-podinfo", + "namespace": "kubeapps", + }, + "spec": map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "sourceRef": map[string]interface{}{ + "kind": "HelmRepository", + "name": "podinfo", + "namespace": "namespace-1", + }, + "version": "> 5", }, }, + "install": map[string]interface{}{ + "createNamespace": true, + }, + "interval": "1m", + "targetNamespace": "test", }, - "install": map[string]interface{}{ - "createNamespace": true, - }, - "interval": "1m0s", - "serviceAccountName": "foo", - "suspend": false, - "targetNamespace": "test", - }, -} + } -var flux_helm_release_values = map[string]interface{}{ - "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", - "kind": "HelmRelease", - "metadata": map[string]interface{}{ - "name": "my-podinfo", - "namespace": "kubeapps", - }, - "spec": map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": "podinfo", - "sourceRef": map[string]interface{}{ - "kind": "HelmRepository", - "name": "podinfo", - "namespace": "namespace-1", + flux_helm_release_reconcile_options = map[string]interface{}{ + "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", + "kind": "HelmRelease", + "metadata": map[string]interface{}{ + "name": "my-podinfo", + "namespace": "kubeapps", + }, + "spec": map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "sourceRef": map[string]interface{}{ + "kind": "HelmRepository", + "name": "podinfo", + "namespace": "namespace-1", + }, }, }, + "install": map[string]interface{}{ + "createNamespace": true, + }, + "interval": "1m0s", + "serviceAccountName": "foo", + "suspend": false, + "targetNamespace": "test", }, - "install": map[string]interface{}{ - "createNamespace": true, + } + + flux_helm_release_values = map[string]interface{}{ + "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", + "kind": "HelmRelease", + "metadata": map[string]interface{}{ + "name": "my-podinfo", + "namespace": "kubeapps", }, - "interval": "1m", - "targetNamespace": "test", - "values": map[string]interface{}{ - "ui": map[string]interface{}{"message": "what we do in the shadows"}, + "spec": map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "sourceRef": map[string]interface{}{ + "kind": "HelmRepository", + "name": "podinfo", + "namespace": "namespace-1", + }, + }, + }, + "install": map[string]interface{}{ + "createNamespace": true, + }, + "interval": "1m", + "targetNamespace": "test", + "values": map[string]interface{}{ + "ui": map[string]interface{}{"message": "what we do in the shadows"}, + }, }, - }, -} + } -var create_installed_package_resp_my_podinfo = &corev1.CreateInstalledPackageResponse{ - InstalledPackageRef: &corev1.InstalledPackageReference{ - Context: &corev1.Context{ - Namespace: "kubeapps", + create_installed_package_resp_my_podinfo = &corev1.CreateInstalledPackageResponse{ + InstalledPackageRef: &corev1.InstalledPackageReference{ + Context: &corev1.Context{ + Namespace: "kubeapps", + }, + Identifier: "my-podinfo", + Plugin: fluxPlugin, }, - Identifier: "my-podinfo", - Plugin: fluxPlugin, - }, -} + } + + flux_helm_release_updated_1 = map[string]interface{}{ + "apiVersion": "helm.toolkit.fluxcd.io/v2beta1", + "kind": "HelmRelease", + "metadata": map[string]interface{}{ + "name": "my-redis", + "namespace": "namespace-1", + "generation": int64(1), + "resourceVersion": "1", + }, + "spec": map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "redis", + "sourceRef": map[string]interface{}{ + "kind": "HelmRepository", + "name": "bitnami-1", + "namespace": "default", + }, + "version": ">14.4.0", + }, + }, + "install": map[string]interface{}{ + "createNamespace": true, + }, + "interval": "1m", + "targetNamespace": "test", + }, + } +) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go index 04435122c02..65ba16d53ab 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go @@ -444,7 +444,7 @@ func (s *Server) UpdateInstalledPackage(ctx context.Context, request *corev1.Upd return nil, status.Errorf(codes.InvalidArgument, "no request InstalledPackageRef provided") } - if err := s.updateRelease( + if installedRef, err := s.updateRelease( ctx, request.InstalledPackageRef, request.PkgVersionReference, @@ -452,6 +452,8 @@ func (s *Server) UpdateInstalledPackage(ctx context.Context, request *corev1.Upd request.Values); err != nil { return nil, err } else { - return &corev1.UpdateInstalledPackageResponse{}, nil + return &corev1.UpdateInstalledPackageResponse{ + InstalledPackageRef: installedRef, + }, nil } } diff --git a/cmd/kubeapps-apis/proto/kubeappsapis/core/packages/v1alpha1/packages.proto b/cmd/kubeapps-apis/proto/kubeappsapis/core/packages/v1alpha1/packages.proto index 0adb18632a1..bb43ae045a4 100644 --- a/cmd/kubeapps-apis/proto/kubeappsapis/core/packages/v1alpha1/packages.proto +++ b/cmd/kubeapps-apis/proto/kubeappsapis/core/packages/v1alpha1/packages.proto @@ -50,7 +50,7 @@ service PackagesService { rpc UpdateInstalledPackage(UpdateInstalledPackageRequest) returns (UpdateInstalledPackageResponse) { option (google.api.http) = { - patch: "/core/packages/v1alpha1/installedpackages" + put: "/core/packages/v1alpha1/installedpackages" body: "*" }; } @@ -160,15 +160,23 @@ message CreateInstalledPackageRequest { // UpdateInstalledPackageRequest // -// Request for UpdateInstalledPackage. Partial resource updates are supported. -// For example, to change the package version one only needs to specify the version reference. -// Similarly to update the values, one only needs to specify that field +// Request for UpdateInstalledPackage. The intent is to reach the desired state specified +// by the fields in the request, while leaving other fields intact. This is a whole +// object "Update" semantics rather than "Patch" semantics. The caller will provide the +// values for the fields fields below, which will replace, or be overlayed onto, the +// corresponding fields in the existing resource. For example, with the +// UpdateInstalledPackageRequest, it is not possible to change just the 'package version +// reference' without also specifying 'values' field. As a side effect, not specifying the +// 'values' field in the request means there are no values specified in the desired state. +// So the meaning of each field value is describing the desired state of the corresponding +// field in the resource after the update operation has completed the renconciliation. message UpdateInstalledPackageRequest { // A reference uniquely identifying the installed package being updated. // Required InstalledPackageReference installed_package_ref = 1; // For helm this will be the exact version in VersionReference.version + // For fluxv2 this could be any semver constraint expression // For other plugins we can extend the VersionReference as needed. Optional VersionReference pkg_version_reference = 2; diff --git a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/fluxv2/packages/v1alpha1/fluxv2.proto b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/fluxv2/packages/v1alpha1/fluxv2.proto index cd4a9a9a1ac..a959bc12ede 100644 --- a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/fluxv2/packages/v1alpha1/fluxv2.proto +++ b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/fluxv2/packages/v1alpha1/fluxv2.proto @@ -58,7 +58,7 @@ service FluxV2PackagesService { // UpdateInstalledPackage updates an installed package based on the request. rpc UpdateInstalledPackage(kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageRequest) returns (kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageResponse) { option (google.api.http) = { - patch: "/plugins/fluxv2/packages/v1alpha1/installedpackages" + put: "/plugins/fluxv2/packages/v1alpha1/installedpackages" body: "*" }; } diff --git a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/helm/packages/v1alpha1/helm.proto b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/helm/packages/v1alpha1/helm.proto index 847a6541bd4..91934f585a6 100644 --- a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/helm/packages/v1alpha1/helm.proto +++ b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/helm/packages/v1alpha1/helm.proto @@ -54,7 +54,7 @@ service HelmPackagesService { // UpdateInstalledPackage updates an installed package based on the request. rpc UpdateInstalledPackage(kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageRequest) returns (kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageResponse) { option (google.api.http) = { - patch: "/plugins/helm/packages/v1alpha1/installedpackages" + put: "/plugins/helm/packages/v1alpha1/installedpackages" body: "*" }; } diff --git a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/kapp_controller/packages/v1alpha1/kapp_controller.proto b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/kapp_controller/packages/v1alpha1/kapp_controller.proto index ea03e88e25d..e714f1ab705 100644 --- a/cmd/kubeapps-apis/proto/kubeappsapis/plugins/kapp_controller/packages/v1alpha1/kapp_controller.proto +++ b/cmd/kubeapps-apis/proto/kubeappsapis/plugins/kapp_controller/packages/v1alpha1/kapp_controller.proto @@ -61,7 +61,7 @@ service KappControllerPackagesService { // UpdateInstalledPackage updates an installed package based on the request. rpc UpdateInstalledPackage(kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageRequest) returns (kubeappsapis.core.packages.v1alpha1.UpdateInstalledPackageResponse) { option (google.api.http) = { - patch: "/plugins/kapp_controller/packages/v1alpha1/installedpackages" + put: "/plugins/kapp_controller/packages/v1alpha1/installedpackages" body: "*" }; }