diff --git a/internal/olm/operator/internal/configmap.go b/internal/olm/operator/internal/configmap.go new file mode 100644 index 00000000000..77903a76545 --- /dev/null +++ b/internal/olm/operator/internal/configmap.go @@ -0,0 +1,149 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 olm + +import ( + "context" + "crypto/md5" + "encoding/base32" + "fmt" + "strings" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + + "github.com/ghodss/yaml" + "github.com/operator-framework/operator-registry/pkg/registry" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + // The directory containing all manifests for an operator, with the + // package manifest being top-level. + containerManifestsDir = "/registry/manifests" +) + +// IsManifestDataStale checks if manifest data stored in the registry is stale +// by comparing it to manifest data currently managed by m. +func (m *RegistryResources) IsManifestDataStale(ctx context.Context, namespace string) (bool, error) { + pkgName := m.Pkg.PackageName + nn := types.NamespacedName{ + Name: getRegistryConfigMapName(pkgName), + Namespace: namespace, + } + configmap := corev1.ConfigMap{} + err := m.Client.KubeClient.Get(ctx, nn, &configmap) + if err != nil { + return false, err + } + // Collect digests of manifests submitted to m. + newData, err := createConfigMapBinaryData(m.Pkg, m.Bundles) + if err != nil { + return false, fmt.Errorf("error creating binary data: %w", err) + } + // If the number of files to be added to the registry don't match the number + // of files currently in the registry, we have added or removed a file. + if len(newData) != len(configmap.BinaryData) { + return true, nil + } + // Check each binary value's key, which contains a base32-encoded md5 digest + // component, against the new set of manifest keys. + for fileKey := range configmap.BinaryData { + if _, match := newData[fileKey]; !match { + return true, nil + } + } + return false, nil +} + +// hashContents creates a base32-encoded md5 digest of b's bytes. +func hashContents(b []byte) string { + h := md5.New() + _, _ = h.Write(b) + enc := base32.StdEncoding.WithPadding(base32.NoPadding) + return enc.EncodeToString(h.Sum(nil)) +} + +// getObjectFileName opaquely creates a unique file name based on data in b. +func getObjectFileName(b []byte, name, kind string) string { + digest := hashContents(b) + return fmt.Sprintf("%s.%s.%s.yaml", digest, name, strings.ToLower(kind)) +} + +func getPackageFileName(b []byte, name string) string { + return getObjectFileName(b, name, "package") +} + +// createConfigMapBinaryData opaquely creates a set of paths using data in pkg +// and each bundle in bundles, unique by path. These paths are intended to +// be keys in a ConfigMap. +func createConfigMapBinaryData(pkg registry.PackageManifest, bundles []*registry.Bundle) (map[string][]byte, error) { + pkgName := pkg.PackageName + binaryKeyValues := map[string][]byte{} + pb, err := yaml.Marshal(pkg) + if err != nil { + return nil, fmt.Errorf("error marshalling package manifest %s: %w", pkgName, err) + } + binaryKeyValues[getPackageFileName(pb, pkgName)] = pb + for _, bundle := range bundles { + for _, o := range bundle.Objects { + ob, err := yaml.Marshal(o) + if err != nil { + return nil, fmt.Errorf("error marshalling object %s %q: %w", o.GroupVersionKind(), o.GetName(), err) + } + binaryKeyValues[getObjectFileName(ob, o.GetName(), o.GetKind())] = ob + } + } + return binaryKeyValues, nil +} + +func getRegistryConfigMapName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-registry-bundles", name) +} + +// withBinaryData returns a function that creates entries in the ConfigMap +// argument's binaryData for each key and []byte value in kvs. +func withBinaryData(kvs map[string][]byte) func(*corev1.ConfigMap) { + return func(cm *corev1.ConfigMap) { + if cm.BinaryData == nil { + cm.BinaryData = map[string][]byte{} + } + for k, v := range kvs { + cm.BinaryData[k] = v + } + } +} + +// newRegistryConfigMap creates a new ConfigMap with a name derived from +// pkgName, the package manifest's packageName, in namespace. opts will +// be applied to the ConfigMap object. +func newRegistryConfigMap(pkgName, namespace string, opts ...func(*corev1.ConfigMap)) *corev1.ConfigMap { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryConfigMapName(pkgName), + Namespace: namespace, + }, + } + for _, opt := range opts { + opt(cm) + } + return cm +} diff --git a/internal/olm/operator/internal/deployment.go b/internal/olm/operator/internal/deployment.go new file mode 100644 index 00000000000..639e2ae0ed0 --- /dev/null +++ b/internal/olm/operator/internal/deployment.go @@ -0,0 +1,176 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 olm + +import ( + "fmt" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // The image operator-registry's initializer and registry-server binaries + // are run from. + // QUESTION(estroz): version registry image? + registryBaseImage = "quay.io/openshift/origin-operator-registry:latest" + // The port registry-server will listen on within a container. + registryGRPCPort = 50051 + // Path of the bundle database generated by initializer. + regisryDBName = "bundle.db" + // Path of the log file generated by registry-server. + // TODO(estroz): have this log file in an obvious place, ex. /var/log. + registryLogFile = "termination.log" +) + +func getRegistryServerName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-registry-server", name) +} + +func getRegistryVolumeName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-bundle-db", name) +} + +// getRegistryDeploymentLabels creates a set of labels to identify +// operator-registry Deployment objects. +func getRegistryDeploymentLabels(pkgName string) map[string]string { + labels := map[string]string{ + "name": getRegistryServerName(pkgName), + } + for k, v := range SDKLabels { + labels[k] = v + } + return labels +} + +// applyToDeploymentPodSpec applies f to dep's pod template spec. +func applyToDeploymentPodSpec(dep *appsv1.Deployment, f func(*corev1.PodSpec)) { + f(&dep.Spec.Template.Spec) +} + +// withVolumeConfigMap returns a function that appends a volume with name +// volName containing a reference to a ConfigMap with name cmName to the +// Deployment argument's pod template spec. +func withVolumeConfigMap(volName, cmName string) func(*appsv1.Deployment) { + volume := corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cmName, + }, + }, + }, + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + spec.Volumes = append(spec.Volumes, volume) + }) + } +} + +// withContainerVolumeMounts returns a function that appends volumeMounts +// to each container in the Deployment argument's pod template spec. One +// volumeMount is appended for each path in paths from volume with name +// volName. +func withContainerVolumeMounts(volName string, paths []string) func(*appsv1.Deployment) { + volumeMounts := []corev1.VolumeMount{} + for _, p := range paths { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: p, + }) + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + for i := range spec.Containers { + spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, volumeMounts...) + } + }) + } +} + +// getDBContainerCmd returns a command string that, when run, does two things: +// 1. Runs a database initializer on the manifests in the current working +// directory. +// 2. Runs an operator-registry server serving the bundle database. +// The database must be in the current working directory. +func getDBContainerCmd(dbPath, logPath string) string { + initCmd := fmt.Sprintf("/usr/bin/initializer -o %s", dbPath) + srvCmd := fmt.Sprintf("/usr/bin/registry-server -d %s -t %s", dbPath, logPath) + return fmt.Sprintf("%s && %s", initCmd, srvCmd) +} + +// withRegistryGRPCContainer returns a function that appends a container +// running an operator-registry GRPC server to the Deployment argument's +// pod template spec. +func withRegistryGRPCContainer(pkgName string) func(*appsv1.Deployment) { + container := corev1.Container{ + Name: getRegistryServerName(pkgName), + Image: registryBaseImage, + Command: []string{"/bin/bash"}, + Args: []string{ + "-c", + // TODO(estroz): grab logs and print if error + getDBContainerCmd(regisryDBName, registryLogFile), + }, + Ports: []corev1.ContainerPort{ + {Name: "registry-grpc", ContainerPort: registryGRPCPort}, + }, + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + spec.Containers = append(spec.Containers, container) + }) + } +} + +// newRegistryDeployment creates a new Deployment with a name derived from +// pkgName, the package manifest's packageName, in namespace. The Deployment +// and replicas are created with labels derived from pkgName. opts will be +// applied to the Deployment object. +func newRegistryDeployment(pkgName, namespace string, opts ...func(*appsv1.Deployment)) *appsv1.Deployment { + var replicas int32 = 1 + dep := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryServerName(pkgName), + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: getRegistryDeploymentLabels(pkgName), + }, + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: getRegistryDeploymentLabels(pkgName), + }, + }, + }, + } + for _, opt := range opts { + opt(dep) + } + return dep +} diff --git a/internal/olm/operator/internal/registry.go b/internal/olm/operator/internal/registry.go new file mode 100644 index 00000000000..135b108dd26 --- /dev/null +++ b/internal/olm/operator/internal/registry.go @@ -0,0 +1,96 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 olm + +import ( + "context" + "fmt" + + olmclient "github.com/operator-framework/operator-sdk/internal/olm/client" + + "github.com/operator-framework/operator-registry/pkg/registry" + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/types" +) + +// SDKLabels are used to identify certain operator-sdk resources. +var SDKLabels = map[string]string{ + "owner": "operator-sdk", +} + +// RegistryResources configures creation/deletion of internal registry-related +// resources. +type RegistryResources struct { + Client *olmclient.Client + Pkg registry.PackageManifest + Bundles []*registry.Bundle +} + +// FEAT(estroz): allow users to specify labels for registry objects. + +// CreateRegistryManifests creates all registry objects required to serve +// manifests from m.manifests in namespace. +func (m *RegistryResources) CreateRegistryManifests(ctx context.Context, namespace string) error { + pkgName := m.Pkg.PackageName + binaryKeyValues, err := createConfigMapBinaryData(m.Pkg, m.Bundles) + if err != nil { + return fmt.Errorf("error creating registry ConfigMap binary data: %w", err) + } + cm := newRegistryConfigMap(pkgName, namespace, + withBinaryData(binaryKeyValues), + ) + volName := getRegistryVolumeName(pkgName) + dep := newRegistryDeployment(pkgName, namespace, + withRegistryGRPCContainer(pkgName), + withVolumeConfigMap(volName, cm.GetName()), + withContainerVolumeMounts(volName, []string{containerManifestsDir}), + ) + service := newRegistryService(pkgName, namespace, + withTCPPort("grpc", registryGRPCPort), + ) + if err = m.Client.DoCreate(ctx, cm, dep, service); err != nil { + return fmt.Errorf("error creating operator %q registry-server objects: %w", pkgName, err) + } + depKey := types.NamespacedName{ + Name: dep.GetName(), + Namespace: namespace, + } + log.Infof("Waiting for Deployment %q rollout to complete", depKey) + if err = m.Client.DoRolloutWait(ctx, depKey); err != nil { + return fmt.Errorf("error waiting for Deployment %q to roll out: %w", depKey, err) + } + return nil +} + +// DeleteRegistryManifests deletes all registry objects serving manifests +// from m.manifests in namespace. +func (m *RegistryResources) DeleteRegistryManifests(ctx context.Context, namespace string) error { + pkgName := m.Pkg.PackageName + cm := newRegistryConfigMap(pkgName, namespace) + dep := newRegistryDeployment(pkgName, namespace) + service := newRegistryService(pkgName, namespace) + err := m.Client.DoDelete(ctx, dep, cm, service) + if err != nil { + return fmt.Errorf("error deleting operator %q registry-server objects: %w", pkgName, err) + } + return nil +} + +// GetRegistryServiceAddr returns a Service's DNS name + port for a given +// pkgName and namespace. +func GetRegistryServiceAddr(pkgName, namespace string) string { + name := getRegistryServerName(pkgName) + return fmt.Sprintf("%s.%s.svc.cluster.local:%d", name, namespace, registryGRPCPort) +} diff --git a/internal/olm/operator/internal/service.go b/internal/olm/operator/internal/service.go new file mode 100644 index 00000000000..064815a2f3d --- /dev/null +++ b/internal/olm/operator/internal/service.go @@ -0,0 +1,57 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 olm + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// withTCPPort returns a function that appends a service port to a Service's +// port list with name and TCP port portNum. +func withTCPPort(name string, portNum int32) func(*corev1.Service) { + return func(service *corev1.Service) { + service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{ + Name: name, + Protocol: corev1.ProtocolTCP, + Port: portNum, + TargetPort: intstr.FromInt(int(portNum)), + }) + } +} + +// newRegistryService creates a new Service with a name derived from pkgName +// the package manifest's packageName, in namespace. The Service is created +// with labels derived from pkgName. opts will be applied to the Service object. +func newRegistryService(pkgName, namespace string, opts ...func(*corev1.Service)) *corev1.Service { + service := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryServerName(pkgName), + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: getRegistryDeploymentLabels(pkgName), + }, + } + for _, opt := range opts { + opt(service) + } + return service +} diff --git a/internal/util/k8sutil/k8sutil.go b/internal/util/k8sutil/k8sutil.go index bb934a29e5b..48ea5bcbd3f 100644 --- a/internal/util/k8sutil/k8sutil.go +++ b/internal/util/k8sutil/k8sutil.go @@ -18,12 +18,14 @@ import ( "bytes" "fmt" "io" + "regexp" "strings" "unicode" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -113,3 +115,15 @@ func GetTypeMetaFromBytes(b []byte) (t metav1.TypeMeta, err error) { Kind: u.GetKind(), }, nil } + +// dns1123LabelRegexp defines the character set allowed in a DNS 1123 label. +var dns1123LabelRegexp = regexp.MustCompile("[^a-zA-Z0-9]+") + +// FormatOperatorNameDNS1123 ensures name is DNS1123 label-compliant by +// replacing all non-compliant UTF-8 characters with "-". +func FormatOperatorNameDNS1123(name string) string { + if len(validation.IsDNS1123Label(name)) != 0 { + return dns1123LabelRegexp.ReplaceAllString(name, "-") + } + return name +}