Skip to content

Expose bundle data from bundle image #94

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
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/opm/alpha/bundle/cmd.go
Original file line number Diff line number Diff line change
@@ -13,5 +13,6 @@ func NewCmd() *cobra.Command {

runCmd.AddCommand(newBundleGenerateCmd())
runCmd.AddCommand(newBundleBuildCmd())
runCmd.AddCommand(extractCmd)
return runCmd
}
65 changes: 65 additions & 0 deletions cmd/opm/alpha/bundle/extract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package bundle

import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/operator-framework/operator-registry/pkg/configmap"
)

var extractCmd = &cobra.Command{
Use: "extract",
Short: "Extracts the data in a bundle directory via ConfigMap",
Long: "Extract takes as input a directory containing manifests and writes the per file contents to a ConfipMap",

PreRunE: func(cmd *cobra.Command, args []string) error {
if debug, _ := cmd.Flags().GetBool("debug"); debug {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
},

RunE: runExtractCmd,
}

func init() {
extractCmd.Flags().Bool("debug", false, "enable debug logging")
extractCmd.Flags().StringP("kubeconfig", "k", "", "absolute path to kubeconfig file")
extractCmd.Flags().StringP("manifestsdir", "m", "/", "path to directory containing manifests")
extractCmd.Flags().StringP("configmapname", "c", "", "name of configmap to write bundle data")
extractCmd.Flags().StringP("namespace", "n", "openshift-operator-lifecycle-manager", "namespace to write configmap data")
extractCmd.Flags().Uint64P("datalimit", "l", 1<<20, "maximum limit in bytes for total bundle data")
extractCmd.MarkPersistentFlagRequired("configmapname")
}

func runExtractCmd(cmd *cobra.Command, args []string) error {
manifestsDir, err := cmd.Flags().GetString("manifestsdir")
if err != nil {
return err
}
kubeconfig, err := cmd.Flags().GetString("kubeconfig")
if err != nil {
return err
}
configmapName, err := cmd.Flags().GetString("configmapname")
if err != nil {
return err
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
return err
}
datalimit, err := cmd.Flags().GetUint64("datalimit")
if err != nil {
return err
}

loader := configmap.NewConfigMapLoaderForDirectory(configmapName, namespace, manifestsDir, kubeconfig)
if err := loader.Populate(datalimit); err != nil {
return fmt.Errorf("error loading manifests from directory: %s", err)
}

return nil
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ require (
github.com/grpc-ecosystem/grpc-health-probe v0.2.1-0.20181220223928-2bf0a5b182db
github.com/imdario/mergo v0.3.7 // indirect
github.com/mattn/go-sqlite3 v1.10.0
github.com/onsi/ginkgo v1.8.0
github.com/onsi/gomega v1.5.0
github.com/otiai10/copy v1.0.1
github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 // indirect
github.com/sirupsen/logrus v1.4.2
26 changes: 2 additions & 24 deletions pkg/appregistry/appregistry.go
Original file line number Diff line number Diff line change
@@ -5,10 +5,8 @@ import (

"github.com/sirupsen/logrus"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

"github.com/operator-framework/operator-registry/pkg/client"
"github.com/operator-framework/operator-registry/pkg/registry"
)

@@ -19,7 +17,7 @@ import (
// downloadPath specifies the folder where the downloaded nested bundle(s) will
// be stored.
func NewLoader(kubeconfig string, dbName string, downloadPath string, logger *logrus.Entry) (*AppregistryLoader, error) {
kubeClient, err := NewKubeClient(kubeconfig, logger)
kubeClient, err := client.NewKubeClient(kubeconfig, logger.Logger)
if err != nil {
return nil, err
}
@@ -104,23 +102,3 @@ func (a *AppregistryLoader) Load(csvSources []string, csvPackages string) (regis

return store, utilerrors.NewAggregate(errs)
}

func NewKubeClient(kubeconfig string, logger *logrus.Entry) (clientset *kubernetes.Clientset, err error) {
var config *rest.Config

if kubeconfig != "" {
logger.Infof("Loading kube client config from path %q", kubeconfig)
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
} else {
logger.Infof("Using in-cluster kube client config")
config, err = rest.InClusterConfig()
}

if err != nil {
err = fmt.Errorf("Cannot load config for REST client: %v", err)
return
}

clientset, err = kubernetes.NewForConfig(config)
return
}
35 changes: 35 additions & 0 deletions pkg/client/kubeclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package client

import (
"fmt"
"os"

"github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

func NewKubeClient(kubeconfig string, logger *logrus.Logger) (clientset *kubernetes.Clientset, err error) {
var config *rest.Config

if overrideConfig := os.Getenv(clientcmd.RecommendedConfigPathEnvVar); overrideConfig != "" {
kubeconfig = overrideConfig
}

if kubeconfig != "" {
logger.Infof("Loading kube client config from path %q", kubeconfig)
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
} else {
logger.Infof("Using in-cluster kube client config")
config, err = rest.InClusterConfig()
}

if err != nil {
err = fmt.Errorf("Cannot load config for REST client: %v", err)
return
}

clientset, err = kubernetes.NewForConfig(config)
return
}
222 changes: 222 additions & 0 deletions pkg/configmap/configmap_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package configmap

import (
"fmt"
"io/ioutil"
"os"
"regexp"

"github.com/ghodss/yaml"
_ "github.com/mattn/go-sqlite3"
"github.com/sirupsen/logrus"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/operator-framework/operator-registry/pkg/client"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
)

// configmap keys can contain underscores, but configmap names can not
var unallowedKeyChars = regexp.MustCompile("[^-A-Za-z0-9_.]")

const (
EnvContainerImage = "CONTAINER_IMAGE"
ConfigMapImageAnnotationKey = "olm.sourceImage"
)

type AnnotationsFile struct {
Annotations struct {
Resources string `json:"operators.operatorframework.io.bundle.manifests.v1"`
MediaType string `json:"operators.operatorframework.io.bundle.mediatype.v1"`
Metadata string `json:"operators.operatorframework.io.bundle.metadata.v1"`
Package string `json:"operators.operatorframework.io.bundle.package.v1"`
Channels string `json:"operators.operatorframework.io.bundle.channels.v1"`
ChannelDefault string `json:"operators.operatorframework.io.bundle.channel.default.v1"`
} `json:"annotations"`
}

type ConfigMapWriter struct {
manifestsDir string
configMapName string
namespace string
clientset *kubernetes.Clientset
}

func NewConfigMapLoaderForDirectory(configMapName, namespace, manifestsDir, kubeconfig string) *ConfigMapWriter {
Copy link
Member

@ecordell ecordell Oct 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: NewConfigMapWriterForDirectory

nit: might want to use the functional option pattern for arguments we've been using elsewhere

clientset, err := client.NewKubeClient(kubeconfig, logrus.StandardLogger())
if err != nil {
logrus.Fatalf("cluster config failed: %v", err)
}

return &ConfigMapWriter{
manifestsDir: manifestsDir,
configMapName: configMapName,
namespace: namespace,
clientset: clientset,
}
}

func TranslateInvalidChars(input string) string {
validConfigMapKey := unallowedKeyChars.ReplaceAllString(input, "~")
return validConfigMapKey
}

func (c *ConfigMapWriter) Populate(maxDataSizeLimit uint64) error {
subDirs := []string{"manifests/", "metadata/"}

configMapPopulate, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Get(c.configMapName, metav1.GetOptions{})
if err != nil {
return err
}
configMapPopulate.Data = map[string]string{}

var totalSize uint64
for _, dir := range subDirs {
completePath := c.manifestsDir + dir
files, err := ioutil.ReadDir(completePath)
if err != nil {
logrus.Errorf("read dir failed: %v", err)
return err
}

for _, file := range files {
log := logrus.WithField("file", completePath+file.Name())
log.Info("Reading file")
content, err := ioutil.ReadFile(completePath + file.Name())
if err != nil {
log.Errorf("read failed: %v", err)
return err
}
totalSize += uint64(len(content))
if totalSize > maxDataSizeLimit {
log.Errorf("File with size %v exceeded %v limit, aboring", len(content), maxDataSizeLimit)
return fmt.Errorf("file %v bigger than total allowed limit", file.Name())
}

validConfigMapKey := TranslateInvalidChars(file.Name())
if validConfigMapKey != file.Name() {
logrus.WithFields(logrus.Fields{
"file.Name": file.Name(),
"validConfigMapKey": validConfigMapKey,
}).Info("translated filename for configmap comptability")
}
if file.Name() == bundle.AnnotationsFile {
var annotationsFile AnnotationsFile
err := yaml.Unmarshal(content, &annotationsFile)
if err != nil {
return err
}
configMapPopulate.SetAnnotations(map[string]string{
bundle.ManifestsLabel: annotationsFile.Annotations.Resources,
bundle.MediatypeLabel: annotationsFile.Annotations.MediaType,
bundle.MetadataLabel: annotationsFile.Annotations.Metadata,
bundle.PackageLabel: annotationsFile.Annotations.Package,
bundle.ChannelsLabel: annotationsFile.Annotations.Channels,
bundle.ChannelDefaultLabel: annotationsFile.Annotations.ChannelDefault,
})
} else {
configMapPopulate.Data[validConfigMapKey] = string(content)
}
}
}

if sourceImage := os.Getenv(EnvContainerImage); sourceImage != "" {
annotations := configMapPopulate.GetAnnotations()
annotations[ConfigMapImageAnnotationKey] = sourceImage
}

_, err = c.clientset.CoreV1().ConfigMaps(c.namespace).Update(configMapPopulate)
if err != nil {
return err
}
return nil
}

// LaunchBundleImage will launch a bundle image and also create a configmap for
// storing the data that will be updated to contain the bundle image data. It is
// the responsibility of the caller to delete the job, the pod, and the configmap
// when done. This function is intended to be called from OLM, but is put here
// for locality.
func LaunchBundleImage(kubeclient kubernetes.Interface, bundleImage, initImage, namespace string) (*corev1.ConfigMap, *batchv1.Job, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should be able to set the path to find opm in initImage?

// create configmap for bundle image data to write to (will be returned)
newConfigMap, err := kubeclient.CoreV1().ConfigMaps(namespace).Create(&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "bundle-image-",
},
})
if err != nil {
return nil, nil, err
}

launchJob := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "deploy-bundle-image-",
},
Spec: batchv1.JobSpec{
//ttlSecondsAfterFinished: 0 // can use in the future to not have to clean up job
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: "bundle-image",
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{
{
Name: "bundle-image",
Image: bundleImage,
ImagePullPolicy: "Never",
Command: []string{"/injected/opm", "alpha", "bundle", "extract", "-n", namespace, "-c", newConfigMap.GetName()},
Env: []corev1.EnvVar{
{
Name: EnvContainerImage,
Value: bundleImage,
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "copydir",
MountPath: "/injected",
},
},
},
},
InitContainers: []corev1.Container{
{
Name: "copy-binary",
Image: initImage,
ImagePullPolicy: "Never",
Command: []string{"/bin/cp", "opm", "/copy-dest"},
VolumeMounts: []corev1.VolumeMount{
{
Name: "copydir",
MountPath: "/copy-dest",
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "copydir",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
},
},
},
}
launchedJob, err := kubeclient.BatchV1().Jobs(namespace).Create(&launchJob)
if err != nil {
err := kubeclient.CoreV1().ConfigMaps(namespace).Delete(newConfigMap.GetName(), &metav1.DeleteOptions{})
if err != nil {
// already in an error, so just report it
logrus.Errorf("failed to remove configmap: %v", err)
}
return nil, nil, err
}

return newConfigMap, launchedJob, nil
}
773 changes: 773 additions & 0 deletions test/e2e/bundle_image_test.go

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package e2e_test

import (
"testing"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)

func TestE2e(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "E2e Suite")
}
5 changes: 5 additions & 0 deletions test/e2e/image-bundle/Dockerfile.bundle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# docker build -t bundle-image .
FROM fedora

COPY manifests /manifests
COPY metadata /metadata
4 changes: 4 additions & 0 deletions test/e2e/image-bundle/Dockerfile.serve
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# docker build -t init-operator-manifest .
FROM busybox

COPY opm /
21 changes: 21 additions & 0 deletions test/e2e/image-bundle/manifests/kiali.crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: kialis.kiali.io
labels:
app: kiali-operator
spec:
group: kiali.io
names:
kind: Kiali
listKind: KialiList
plural: kialis
singular: kiali
scope: Namespaced
subresources:
status: {}
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: monitoringdashboards.monitoring.kiali.io
labels:
app: kiali
spec:
group: monitoring.kiali.io
names:
kind: MonitoringDashboard
listKind: MonitoringDashboardList
plural: monitoringdashboards
singular: monitoringdashboard
scope: Namespaced
version: v1alpha1
7 changes: 7 additions & 0 deletions test/e2e/image-bundle/manifests/kiali.package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
packageName: kiali
channels:
- name: alpha
currentCSV: kiali-operator.v1.4.2
- name: stable
currentCSV: kiali-operator.v1.4.2
defaultChannel: stable

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions test/e2e/image-bundle/metadata/annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
annotations:
operators.operatorframework.io.bundle.mediatype.v1: "registry+v1"
operators.operatorframework.io.bundle.manifests.v1: "/manifests/"
operators.operatorframework.io.bundle.metadata.v1: "/metadata/"
operators.operatorframework.io.bundle.package.v1: "kiali-operator.v1.4.2"
operators.operatorframework.io.bundle.channels.v1: "alpha,stable"
operators.operatorframework.io.bundle.channel.default.v1: "stable"