diff --git a/Makefile b/Makefile index b8ebd6f21..f0777d29e 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ VERSION ?= 0.0.1 bundle \ bundle-build +ARTIFACT_DIR ?= . SOURCES := $(shell find . -name '*.go' -not -path "*/vendor/*") GOBUILDFLAGS ?= -i -mod=vendor GOLDFLAGS ?= -s -w -X github.com/openshift/cincinnati-operator/version.Operator=$(VERSION) @@ -53,7 +54,7 @@ deploy: func-test: deploy @echo "Running functional test suite" go clean -testcache - go test -timeout 20m -v ./functests/... + go test -timeout 20m -v ./functests/... || (oc -n openshift-updateservice adm inspect --dest-dir="$(ARTIFACT_DIR)/inspect" namespace/openshift-updateservice customresourcedefinition/updateservices.updateservice.operator.openshift.io updateservice/example; false) unit-test: @echo "Executing unit tests" diff --git a/api/v1/updateservice_types.go b/api/v1/updateservice_types.go index df60cb2ac..7f114bd51 100644 --- a/api/v1/updateservice_types.go +++ b/api/v1/updateservice_types.go @@ -39,7 +39,14 @@ type UpdateServiceStatus struct { // engine. Available paths from this URI include: // // * /api/upgrades_info/v1/graph, with the update graph recommendations. + // * /api/upgrades_info/graph, with the update graph recommendations, versioned by content-type (e.g. application/vnd.redhat.cincinnati.v1+json). PolicyEngineURI string `json:"policyEngineURI,optional"` + + // metadataURI is the external URI which exposes metadata. + // Available paths from this URI include: + // + // * /api/upgrades_info/signatures/{ALGORITHM}/{DIGEST}/{SIGNATURE}, with release signatures. + MetadataURI string `json:"metadataURI,optional"` } // Condition Types diff --git a/config/crd/bases/updateservice.operator.openshift.io_updateservices.yaml b/config/crd/bases/updateservice.operator.openshift.io_updateservices.yaml index 30d6facc1..3b7c9403a 100644 --- a/config/crd/bases/updateservice.operator.openshift.io_updateservices.yaml +++ b/config/crd/bases/updateservice.operator.openshift.io_updateservices.yaml @@ -1,11 +1,9 @@ - --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.4.1 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: updateservices.updateservice.operator.openshift.io spec: group: updateservice.operator.openshift.io @@ -31,6 +29,8 @@ spec: object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string + metadata: + type: object spec: description: spec is the desired state of the UpdateService service. The operator will work to ensure that the desired configuration is applied @@ -87,12 +87,20 @@ spec: - type type: object type: array + metadataURI: + description: "metadataURI is the external URI which exposes metadata. + Available paths from this URI include: \n * /api/upgrades_info/signatures/{ALGORITHM}/{DIGEST}/{SIGNATURE}, + with release signatures." + type: string policyEngineURI: description: "policyEngineURI is the external URI which exposes the policy engine. Available paths from this URI include: \n * /api/upgrades_info/v1/graph, - with the update graph recommendations." + with the update graph recommendations. * /api/upgrades_info/graph, + with the update graph recommendations, versioned by content-type + (e.g. application/vnd.redhat.cincinnati.v1+json)." type: string required: + - metadataURI - policyEngineURI type: object required: @@ -100,7 +108,7 @@ spec: - spec type: object additionalPrinterColumns: - - name: Age + - name: Age description: The age of the UpdateService resource. type: date jsonPath: .metadata.creationTimestamp @@ -128,9 +136,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8185ba9cb..88f301d5c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -1,9 +1,7 @@ - --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: updateservice-operator rules: - apiGroups: diff --git a/controllers/names.go b/controllers/names.go index 44684ce47..86c5677e5 100644 --- a/controllers/names.go +++ b/controllers/names.go @@ -10,6 +10,8 @@ const ( NameContainerGraphBuilder string = "graph-builder" // NameContainerPolicyEngine is the Name property of the policy engine container NameContainerPolicyEngine string = "policy-engine" + // NameContainerMetadata is the Name property of the metadata container + NameContainerMetadata string = "metadata" // NameInitContainerGraphData is the Name property of the graph data container NameInitContainerGraphData string = "graph-data" // OpenshiftConfigNamespace is the name of openshift's configuration namespace @@ -48,6 +50,10 @@ func namePolicyEngineService(instance *cv1.UpdateService) string { return instance.Name + "-policy-engine" } +func nameMetadataService(instance *cv1.UpdateService) string { + return instance.Name + "-metadata" +} + func nameGraphBuilderService(instance *cv1.UpdateService) string { return instance.Name + "-graph-builder" } @@ -60,6 +66,10 @@ func oldPolicyEngineRouteName(instance *cv1.UpdateService) string { return namePolicyEngineService(instance) + "-route" } +func nameMetadataRoute(instance *cv1.UpdateService) string { + return instance.Name + "-meta-route" +} + func nameAdditionalTrustedCA(instance *cv1.UpdateService) string { return instance.Name + "-trusted-ca" } diff --git a/controllers/new.go b/controllers/new.go index 79e6798b5..5a67b74dd 100644 --- a/controllers/new.go +++ b/controllers/new.go @@ -81,10 +81,13 @@ type kubeResources struct { graphBuilderContainer *corev1.Container graphDataInitContainer *corev1.Container policyEngineContainer *corev1.Container + metadataContainer *corev1.Container graphBuilderService *corev1.Service policyEngineService *corev1.Service + metadataService *corev1.Service policyEngineRoute *routev1.Route policyEngineOldRoute *routev1.Route + metadataRoute *routev1.Route trustedCAConfig *corev1.ConfigMap trustedClusterCAConfig *corev1.ConfigMap pullSecret *corev1.Secret @@ -123,11 +126,14 @@ func newKubeResources(instance *cv1.UpdateService, image string, pullSecret *cor k.graphBuilderContainer = k.newGraphBuilderContainer(instance, image) k.graphDataInitContainer = k.newGraphDataInitContainer(instance) k.policyEngineContainer = k.newPolicyEngineContainer(instance, image) + k.metadataContainer = k.newMetadataContainer(instance, image) k.deployment = k.newDeployment(instance) k.graphBuilderService = k.newGraphBuilderService(instance) k.policyEngineService = k.newPolicyEngineService(instance) + k.metadataService = k.newMetadataService(instance) k.policyEngineRoute = k.newPolicyEngineRoute(instance) k.policyEngineOldRoute = k.oldPolicyEngineRoute(instance) + k.metadataRoute = k.newMetadataRoute(instance) return &k, nil } @@ -217,6 +223,40 @@ func (k *kubeResources) newPolicyEngineService(instance *cv1.UpdateService) *cor } } +func (k *kubeResources) newMetadataService(instance *cv1.UpdateService) *corev1.Service { + name := nameMetadataService(instance) + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: instance.Namespace, + Labels: map[string]string{ + "app": name, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "metadata", + Port: 80, + TargetPort: intstr.FromInt(8082), + Protocol: corev1.ProtocolTCP, + }, + { + Name: "status-m", + Port: 9082, + TargetPort: intstr.FromInt(9082), + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{ + "deployment": nameDeployment(instance), + }, + SessionAffinity: corev1.ServiceAffinityNone, + }, + } +} + func (k *kubeResources) newPolicyEngineRoute(instance *cv1.UpdateService) *routev1.Route { name := namePolicyEngineRoute(instance) return &routev1.Route{ @@ -269,6 +309,32 @@ func (k *kubeResources) oldPolicyEngineRoute(instance *cv1.UpdateService) *route } } +func (k *kubeResources) newMetadataRoute(instance *cv1.UpdateService) *routev1.Route { + name := nameMetadataRoute(instance) + return &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: instance.Namespace, + Labels: map[string]string{ + "app": nameDeployment(instance), + }, + }, + Spec: routev1.RouteSpec{ + Port: &routev1.RoutePort{ + TargetPort: intstr.FromString("metadata"), + }, + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: nameMetadataService(instance), + }, + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyNone, + }, + }, + } +} + func (k *kubeResources) newEnvConfig(instance *cv1.UpdateService) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -283,6 +349,7 @@ func (k *kubeResources) newEnvConfig(instance *cv1.UpdateService) *corev1.Config "pe.rust_backtrace": "0", "pe.status.address": "::", "pe.upstream": "http://localhost:8080/v1/graph", + "m.rust_backtrace": "0", }, } } @@ -357,6 +424,7 @@ func (k *kubeResources) newDeployment(instance *cv1.UpdateService) *appsv1.Deplo Containers: []corev1.Container{ *k.graphBuilderContainer, *k.policyEngineContainer, + *k.metadataContainer, }, }, }, @@ -734,6 +802,92 @@ func (k *kubeResources) newPolicyEngineContainer(instance *cv1.UpdateService, im } } +func (k *kubeResources) newMetadataContainer(instance *cv1.UpdateService, image string) *corev1.Container { + envConfigName := nameEnvConfig(instance) + return &corev1.Container{ + Name: NameContainerMetadata, + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/usr/bin/metadata-helper", + }, + Args: []string{ + "-vvv", + "--signatures.dir", + "/var/lib/cincinnati/graph-data/signatures", + "--service.address", + "::", + "--service.port", + "8082", + "--service.path_prefix", + "/api/upgrades_info", + "--status.address", + "::", + "--status.port", + "9082", + }, + Ports: []corev1.ContainerPort{ + { + Name: "metadata", + ContainerPort: 8082, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "status-m", + ContainerPort: 9082, + Protocol: corev1.ProtocolTCP, + }, + }, + Env: []corev1.EnvVar{ + newCMEnvVar("RUST_BACKTRACE", "m.rust_backtrace", envConfigName), + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(750, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(768*1024*1024, resource.BinarySI), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(350, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(128*1024*1024, resource.BinarySI), + }, + }, + LivenessProbe: &corev1.Probe{ + FailureThreshold: 3, + SuccessThreshold: 1, + InitialDelaySeconds: 150, + PeriodSeconds: 30, + TimeoutSeconds: 3, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/livez", + Port: intstr.FromInt(9082), + Scheme: corev1.URISchemeHTTP, + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + FailureThreshold: 3, + SuccessThreshold: 1, + InitialDelaySeconds: 150, + PeriodSeconds: 30, + TimeoutSeconds: 3, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/readyz", + Port: intstr.FromInt(9082), + Scheme: corev1.URISchemeHTTP, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "cincinnati-graph-data", + MountPath: "/var/lib/cincinnati/graph-data", + }, + }, + } +} + func newCMEnvVar(name, key, cmName string) corev1.EnvVar { return corev1.EnvVar{ Name: name, diff --git a/controllers/updateservice_controller.go b/controllers/updateservice_controller.go index afe7687fb..ef5655c9b 100644 --- a/controllers/updateservice_controller.go +++ b/controllers/updateservice_controller.go @@ -161,8 +161,10 @@ func (r *UpdateServiceReconciler) Reconcile(ctx context.Context, req ctrl.Reques r.ensureAdditionalTrustedCA, r.ensureGraphBuilderService, r.ensurePolicyEngineService, + r.ensureMetadataService, r.ensurePodDisruptionBudget, r.ensurePolicyEngineRoute, + r.ensureMetadataRoute, } { err = f(ctx, reqLogger, instanceCopy, resources) if err != nil { @@ -638,6 +640,20 @@ func (r *UpdateServiceReconciler) ensureGraphBuilderService(ctx context.Context, return nil } +func (r *UpdateServiceReconciler) ensureMetadataService(ctx context.Context, reqLogger logr.Logger, instance *cv1.UpdateService, resources *kubeResources) error { + service := resources.metadataService + // Set UpdateService instance as the owner and controller + if err := controllerutil.SetControllerReference(instance, service, r.Scheme); err != nil { + return err + } + + if err := r.ensureService(ctx, reqLogger, service); err != nil { + handleErr(reqLogger, &instance.Status, "EnsureServiceFailed", err) + return err + } + return nil +} + func (r *UpdateServiceReconciler) ensurePolicyEngineService(ctx context.Context, reqLogger logr.Logger, instance *cv1.UpdateService, resources *kubeResources) error { service := resources.policyEngineService // Set UpdateService instance as the owner and controller @@ -674,8 +690,54 @@ func validateRouteName(instance *cv1.UpdateService, name string, namespace strin return fmt.Errorf(fmt.Sprintf("UpdateService route name %q %s Route name %s", routeName, errReasons[0], errReasons[1])) } -func (r *UpdateServiceReconciler) ensurePolicyEngineRoute(ctx context.Context, reqLogger logr.Logger, instance *cv1.UpdateService, resources *kubeResources) error { +func (r *UpdateServiceReconciler) ensureMetadataRoute(ctx context.Context, reqLogger logr.Logger, instance *cv1.UpdateService, resources *kubeResources) error { + route := resources.metadataRoute + foundRoute := &routev1.Route{} + err := r.Client.Get(ctx, types.NamespacedName{Name: route.Name, Namespace: route.Namespace}, foundRoute) + if err != nil && apiErrors.IsNotFound(err) { + // Set UpdateService instance as the owner and controller + if err = controllerutil.SetControllerReference(instance, route, r.Scheme); err != nil { + return err + } + reqLogger.Info("Creating Route", "Namespace", route.Namespace, "Name", route.Name) + if err = r.Client.Create(ctx, route); err != nil { + handleErr(reqLogger, &instance.Status, "CreateRouteFailed", err) + } + return err + } else if err != nil { + handleErr(reqLogger, &instance.Status, "GetRouteFailed", err) + return err + } + if uri, _, err := routeapihelpers.IngressURI(foundRoute, ""); err == nil { + instance.Status.MetadataURI = uri.String() + } else { + handleErr(reqLogger, &instance.Status, "RouteIngressFailed", err) + } + + updated := foundRoute.DeepCopy() + // Keep found tls for later use + tls := updated.Spec.TLS + // This is just so we compare the Spec on the two objects but make an exception for Spec.TLS + updated.Spec.TLS = route.Spec.TLS + + // found existing resource; let's compare and update if needed + if !reflect.DeepEqual(updated.Spec, route.Spec) { + reqLogger.Info("Updating Route", "Namespace", route.Namespace, "Name", route.Name) + updated.Spec = route.Spec + // We want to allow user to update the TLS cert/key manually on the route and we don't want to override that change. + // Keep the existing tls on the route + updated.Spec.TLS = tls + err = r.Client.Update(ctx, updated) + if err != nil { + handleErr(reqLogger, &instance.Status, "UpdateRouteFailed", err) + } + } + + return nil +} + +func (r *UpdateServiceReconciler) ensurePolicyEngineRoute(ctx context.Context, reqLogger logr.Logger, instance *cv1.UpdateService, resources *kubeResources) error { route := resources.policyEngineRoute foundRoute, err := r.findExistingRoute(ctx, reqLogger, instance, resources) if err != nil { diff --git a/dev/Dockerfile b/dev/Dockerfile index 0164623fd..77088ad95 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -4,4 +4,6 @@ RUN curl -L -o cincinnati-graph-data.tar.gz https://api.openshift.com/api/upgrad RUN mkdir -p /var/lib/cincinnati-graph-data && tar xvzf cincinnati-graph-data.tar.gz -C /var/lib/cincinnati-graph-data/ --no-overwrite-dir --no-same-owner -CMD ["/bin/bash", "-c" ,"exec cp -rp /var/lib/cincinnati-graph-data/* /var/lib/cincinnati/graph-data"] +RUN mkdir -p /var/lib/cincinnati-graph-data/signatures/sha256/beda83fb057e328d6f94f8415382350ca3ddf99bb9094e262184e0f127810ce0 && curl -L https://mirror.openshift.com/pub/openshift-v4/signatures/openshift/release/sha256=beda83fb057e328d6f94f8415382350ca3ddf99bb9094e262184e0f127810ce0/signature-1 >/var/lib/cincinnati-graph-data/signatures/sha256/beda83fb057e328d6f94f8415382350ca3ddf99bb9094e262184e0f127810ce0/signature-1 && echo 1.2.0 >/var/lib/cincinnati-graph-data/version + +CMD ["/bin/bash", "-c" ,"exec cp -rpv /var/lib/cincinnati-graph-data/* /var/lib/cincinnati/graph-data"] diff --git a/functests/updateservice_creation_test.go b/functests/updateservice_creation_test.go index 632c39696..b7d730af4 100644 --- a/functests/updateservice_creation_test.go +++ b/functests/updateservice_creation_test.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net/http" + "os" "testing" updateservicev1 "github.com/openshift/cincinnati-operator/api/v1" @@ -110,6 +111,10 @@ func TestCustomResource(t *testing.T) { t.Fatal(err) } + if err := waitForService(ctx, k8sClient, customResourceName+"-metadata"); err != nil { + t.Fatal(err) + } + // Checks to see if a given PodDisruptionBudget is available after a specified amount of time. // If the PodDisruptionBudget is not available after 30 * retries seconds, the condition function returns an error. if err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { @@ -127,6 +132,7 @@ func TestCustomResource(t *testing.T) { t.Logf("PodDisruptionBudget %s available", operatorName) var policyEngineURI string + var metadataURI string if err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { result := &updateservicev1.UpdateService{} err = updateServiceClient.Get(). @@ -140,6 +146,11 @@ func TestCustomResource(t *testing.T) { } if result.Status.PolicyEngineURI != "" { policyEngineURI = result.Status.PolicyEngineURI + } + if result.Status.MetadataURI != "" { + metadataURI = result.Status.MetadataURI + } + if policyEngineURI != "" && metadataURI != "" { return true, nil } return false, nil @@ -147,13 +158,14 @@ func TestCustomResource(t *testing.T) { t.Fatal(err) } t.Logf("Policy engine route available at %s", policyEngineURI) + t.Logf("Metadata route available at %s", metadataURI) tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } httpClient := &http.Client{Transport: tr} - graphURI := fmt.Sprintf("%s/api/upgrades_info/v1/graph?channel=stable-4.4", policyEngineURI) + graphURI := fmt.Sprintf("%s/api/upgrades_info/graph?channel=stable-4.13", policyEngineURI) req, err := http.NewRequestWithContext(ctx, "GET", graphURI, nil) if err != nil { t.Fatal(err) @@ -162,8 +174,8 @@ func TestCustomResource(t *testing.T) { if err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { if resp, err := httpClient.Do(req); err != nil { t.Fatal(err) - } else if resp.StatusCode > 200 { - t.Logf("Waiting for availability of policy engine%s", graphURI) + } else if resp.StatusCode > http.StatusOK { + t.Logf("Waiting for availability of policy engine %s", graphURI) return false, nil } t.Logf("Policy engine %s available", graphURI) @@ -172,9 +184,30 @@ func TestCustomResource(t *testing.T) { t.Fatal(err) } - ctx = context.Background() + graph_data := os.Getenv("GRAPH_DATA") + signatureURI := fmt.Sprintf("%s/api/upgrades_info/signatures/sha256=beda83fb057e328d6f94f8415382350ca3ddf99bb9094e262184e0f127810ce0/signature-1", metadataURI) + req, err = http.NewRequestWithContext(ctx, "GET", signatureURI, nil) + if err != nil { + t.Fatal(err) + } + if err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { + if resp, err := httpClient.Do(req); err != nil { + t.Fatal(err) + } else if graph_data == "local" && resp.StatusCode == http.StatusOK { + t.Logf("Signature %s available, as expected for GRAPH_DATA=%q", signatureURI, graph_data) + return true, nil + } else if graph_data != "local" && resp.StatusCode == http.StatusNotFound { + t.Logf("Signature %s not available, as expected for GRAPH_DATA=%q", signatureURI, graph_data) + return true, nil + } else { + t.Logf("Waiting for availability of signature %s (current status %q)", signatureURI, resp.Status) + } + return false, nil + }); err != nil { + t.Fatal(err) + } + if err := deleteCR(ctx); err != nil { t.Log(err) } - }