-
Notifications
You must be signed in to change notification settings - Fork 1.8k
internal/olm/operator/internal: operator-registry wrappers #2313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
estroz
merged 9 commits into
operator-framework:master
from
estroz:feature/olm-poc-registry
Jan 8, 2020
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
28fa846
internal/olm/operator/internal: tools to create and delete a manifest…
estroz 68e322d
use PackageManifest and Bundle types directly
estroz f049b3d
test/test-framework: revendor
estroz bc08d4e
use getObjectFileName in getPackageFileName
estroz 2d03ce3
use fmt.Errorf over errors.Wrap
estroz c4a2df7
Merge branch 'master' into feature/olm-poc-registry
estroz 64efcf9
Merge branch 'master' into feature/olm-poc-registry
estroz 8a282ae
Merge branch 'master' into feature/olm-poc-registry
estroz 8601769
remove go.sum duplicate entry
estroz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
estroz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the path is internal/olm/operator/internal again?
Could not it be just
internal/olm/registry
since what all it is doing is creating the Registry?Also, it if is a public API then, I understand that, should be in the pkg instead of internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be
internal/olm/registry
, but this package is only intended to be used by code ininternal/olm/operator
, hence its current path. If that changes in the future we can move it. I recommend going through #1912 to get a handle on why this code is set up as-is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. But it shows strange internal/.../internal again.
WDYT about
internal/olm/operator/registry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't a common use case but it is a completely viable use of
internal
.