Skip to content

Commit 8df762a

Browse files
Merge pull request #94 from jpeeler/expose-bundle-data
Expose bundle data from bundle image
2 parents cdfd65b + 2c7ba05 commit 8df762a

File tree

15 files changed

+1727
-24
lines changed

15 files changed

+1727
-24
lines changed

cmd/opm/alpha/bundle/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ func NewCmd() *cobra.Command {
1313

1414
runCmd.AddCommand(newBundleGenerateCmd())
1515
runCmd.AddCommand(newBundleBuildCmd())
16+
runCmd.AddCommand(extractCmd)
1617
return runCmd
1718
}

cmd/opm/alpha/bundle/extract.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package bundle
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/operator-framework/operator-registry/pkg/configmap"
10+
)
11+
12+
var extractCmd = &cobra.Command{
13+
Use: "extract",
14+
Short: "Extracts the data in a bundle directory via ConfigMap",
15+
Long: "Extract takes as input a directory containing manifests and writes the per file contents to a ConfipMap",
16+
17+
PreRunE: func(cmd *cobra.Command, args []string) error {
18+
if debug, _ := cmd.Flags().GetBool("debug"); debug {
19+
logrus.SetLevel(logrus.DebugLevel)
20+
}
21+
return nil
22+
},
23+
24+
RunE: runExtractCmd,
25+
}
26+
27+
func init() {
28+
extractCmd.Flags().Bool("debug", false, "enable debug logging")
29+
extractCmd.Flags().StringP("kubeconfig", "k", "", "absolute path to kubeconfig file")
30+
extractCmd.Flags().StringP("manifestsdir", "m", "/", "path to directory containing manifests")
31+
extractCmd.Flags().StringP("configmapname", "c", "", "name of configmap to write bundle data")
32+
extractCmd.Flags().StringP("namespace", "n", "openshift-operator-lifecycle-manager", "namespace to write configmap data")
33+
extractCmd.Flags().Uint64P("datalimit", "l", 1<<20, "maximum limit in bytes for total bundle data")
34+
extractCmd.MarkPersistentFlagRequired("configmapname")
35+
}
36+
37+
func runExtractCmd(cmd *cobra.Command, args []string) error {
38+
manifestsDir, err := cmd.Flags().GetString("manifestsdir")
39+
if err != nil {
40+
return err
41+
}
42+
kubeconfig, err := cmd.Flags().GetString("kubeconfig")
43+
if err != nil {
44+
return err
45+
}
46+
configmapName, err := cmd.Flags().GetString("configmapname")
47+
if err != nil {
48+
return err
49+
}
50+
namespace, err := cmd.Flags().GetString("namespace")
51+
if err != nil {
52+
return err
53+
}
54+
datalimit, err := cmd.Flags().GetUint64("datalimit")
55+
if err != nil {
56+
return err
57+
}
58+
59+
loader := configmap.NewConfigMapLoaderForDirectory(configmapName, namespace, manifestsDir, kubeconfig)
60+
if err := loader.Populate(datalimit); err != nil {
61+
return fmt.Errorf("error loading manifests from directory: %s", err)
62+
}
63+
64+
return nil
65+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ require (
1010
github.com/grpc-ecosystem/grpc-health-probe v0.2.1-0.20181220223928-2bf0a5b182db
1111
github.com/imdario/mergo v0.3.7 // indirect
1212
github.com/mattn/go-sqlite3 v1.10.0
13+
github.com/onsi/ginkgo v1.8.0
14+
github.com/onsi/gomega v1.5.0
1315
github.com/otiai10/copy v1.0.1
1416
github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 // indirect
1517
github.com/sirupsen/logrus v1.4.2

pkg/appregistry/appregistry.go

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import (
55

66
"github.com/sirupsen/logrus"
77
utilerrors "k8s.io/apimachinery/pkg/util/errors"
8-
"k8s.io/client-go/kubernetes"
9-
"k8s.io/client-go/rest"
10-
"k8s.io/client-go/tools/clientcmd"
118

9+
"github.com/operator-framework/operator-registry/pkg/client"
1210
"github.com/operator-framework/operator-registry/pkg/registry"
1311
)
1412

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

105103
return store, utilerrors.NewAggregate(errs)
106104
}
107-
108-
func NewKubeClient(kubeconfig string, logger *logrus.Entry) (clientset *kubernetes.Clientset, err error) {
109-
var config *rest.Config
110-
111-
if kubeconfig != "" {
112-
logger.Infof("Loading kube client config from path %q", kubeconfig)
113-
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
114-
} else {
115-
logger.Infof("Using in-cluster kube client config")
116-
config, err = rest.InClusterConfig()
117-
}
118-
119-
if err != nil {
120-
err = fmt.Errorf("Cannot load config for REST client: %v", err)
121-
return
122-
}
123-
124-
clientset, err = kubernetes.NewForConfig(config)
125-
return
126-
}

pkg/client/kubeclient.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/sirupsen/logrus"
8+
"k8s.io/client-go/kubernetes"
9+
"k8s.io/client-go/rest"
10+
"k8s.io/client-go/tools/clientcmd"
11+
)
12+
13+
func NewKubeClient(kubeconfig string, logger *logrus.Logger) (clientset *kubernetes.Clientset, err error) {
14+
var config *rest.Config
15+
16+
if overrideConfig := os.Getenv(clientcmd.RecommendedConfigPathEnvVar); overrideConfig != "" {
17+
kubeconfig = overrideConfig
18+
}
19+
20+
if kubeconfig != "" {
21+
logger.Infof("Loading kube client config from path %q", kubeconfig)
22+
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
23+
} else {
24+
logger.Infof("Using in-cluster kube client config")
25+
config, err = rest.InClusterConfig()
26+
}
27+
28+
if err != nil {
29+
err = fmt.Errorf("Cannot load config for REST client: %v", err)
30+
return
31+
}
32+
33+
clientset, err = kubernetes.NewForConfig(config)
34+
return
35+
}

pkg/configmap/configmap_writer.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package configmap
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"regexp"
8+
9+
"github.com/ghodss/yaml"
10+
_ "github.com/mattn/go-sqlite3"
11+
"github.com/sirupsen/logrus"
12+
batchv1 "k8s.io/api/batch/v1"
13+
corev1 "k8s.io/api/core/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/client-go/kubernetes"
16+
17+
"github.com/operator-framework/operator-registry/pkg/client"
18+
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
19+
)
20+
21+
// configmap keys can contain underscores, but configmap names can not
22+
var unallowedKeyChars = regexp.MustCompile("[^-A-Za-z0-9_.]")
23+
24+
const (
25+
EnvContainerImage = "CONTAINER_IMAGE"
26+
ConfigMapImageAnnotationKey = "olm.sourceImage"
27+
)
28+
29+
type AnnotationsFile struct {
30+
Annotations struct {
31+
Resources string `json:"operators.operatorframework.io.bundle.manifests.v1"`
32+
MediaType string `json:"operators.operatorframework.io.bundle.mediatype.v1"`
33+
Metadata string `json:"operators.operatorframework.io.bundle.metadata.v1"`
34+
Package string `json:"operators.operatorframework.io.bundle.package.v1"`
35+
Channels string `json:"operators.operatorframework.io.bundle.channels.v1"`
36+
ChannelDefault string `json:"operators.operatorframework.io.bundle.channel.default.v1"`
37+
} `json:"annotations"`
38+
}
39+
40+
type ConfigMapWriter struct {
41+
manifestsDir string
42+
configMapName string
43+
namespace string
44+
clientset *kubernetes.Clientset
45+
}
46+
47+
func NewConfigMapLoaderForDirectory(configMapName, namespace, manifestsDir, kubeconfig string) *ConfigMapWriter {
48+
clientset, err := client.NewKubeClient(kubeconfig, logrus.StandardLogger())
49+
if err != nil {
50+
logrus.Fatalf("cluster config failed: %v", err)
51+
}
52+
53+
return &ConfigMapWriter{
54+
manifestsDir: manifestsDir,
55+
configMapName: configMapName,
56+
namespace: namespace,
57+
clientset: clientset,
58+
}
59+
}
60+
61+
func TranslateInvalidChars(input string) string {
62+
validConfigMapKey := unallowedKeyChars.ReplaceAllString(input, "~")
63+
return validConfigMapKey
64+
}
65+
66+
func (c *ConfigMapWriter) Populate(maxDataSizeLimit uint64) error {
67+
subDirs := []string{"manifests/", "metadata/"}
68+
69+
configMapPopulate, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Get(c.configMapName, metav1.GetOptions{})
70+
if err != nil {
71+
return err
72+
}
73+
configMapPopulate.Data = map[string]string{}
74+
75+
var totalSize uint64
76+
for _, dir := range subDirs {
77+
completePath := c.manifestsDir + dir
78+
files, err := ioutil.ReadDir(completePath)
79+
if err != nil {
80+
logrus.Errorf("read dir failed: %v", err)
81+
return err
82+
}
83+
84+
for _, file := range files {
85+
log := logrus.WithField("file", completePath+file.Name())
86+
log.Info("Reading file")
87+
content, err := ioutil.ReadFile(completePath + file.Name())
88+
if err != nil {
89+
log.Errorf("read failed: %v", err)
90+
return err
91+
}
92+
totalSize += uint64(len(content))
93+
if totalSize > maxDataSizeLimit {
94+
log.Errorf("File with size %v exceeded %v limit, aboring", len(content), maxDataSizeLimit)
95+
return fmt.Errorf("file %v bigger than total allowed limit", file.Name())
96+
}
97+
98+
validConfigMapKey := TranslateInvalidChars(file.Name())
99+
if validConfigMapKey != file.Name() {
100+
logrus.WithFields(logrus.Fields{
101+
"file.Name": file.Name(),
102+
"validConfigMapKey": validConfigMapKey,
103+
}).Info("translated filename for configmap comptability")
104+
}
105+
if file.Name() == bundle.AnnotationsFile {
106+
var annotationsFile AnnotationsFile
107+
err := yaml.Unmarshal(content, &annotationsFile)
108+
if err != nil {
109+
return err
110+
}
111+
configMapPopulate.SetAnnotations(map[string]string{
112+
bundle.ManifestsLabel: annotationsFile.Annotations.Resources,
113+
bundle.MediatypeLabel: annotationsFile.Annotations.MediaType,
114+
bundle.MetadataLabel: annotationsFile.Annotations.Metadata,
115+
bundle.PackageLabel: annotationsFile.Annotations.Package,
116+
bundle.ChannelsLabel: annotationsFile.Annotations.Channels,
117+
bundle.ChannelDefaultLabel: annotationsFile.Annotations.ChannelDefault,
118+
})
119+
} else {
120+
configMapPopulate.Data[validConfigMapKey] = string(content)
121+
}
122+
}
123+
}
124+
125+
if sourceImage := os.Getenv(EnvContainerImage); sourceImage != "" {
126+
annotations := configMapPopulate.GetAnnotations()
127+
annotations[ConfigMapImageAnnotationKey] = sourceImage
128+
}
129+
130+
_, err = c.clientset.CoreV1().ConfigMaps(c.namespace).Update(configMapPopulate)
131+
if err != nil {
132+
return err
133+
}
134+
return nil
135+
}
136+
137+
// LaunchBundleImage will launch a bundle image and also create a configmap for
138+
// storing the data that will be updated to contain the bundle image data. It is
139+
// the responsibility of the caller to delete the job, the pod, and the configmap
140+
// when done. This function is intended to be called from OLM, but is put here
141+
// for locality.
142+
func LaunchBundleImage(kubeclient kubernetes.Interface, bundleImage, initImage, namespace string) (*corev1.ConfigMap, *batchv1.Job, error) {
143+
// create configmap for bundle image data to write to (will be returned)
144+
newConfigMap, err := kubeclient.CoreV1().ConfigMaps(namespace).Create(&corev1.ConfigMap{
145+
ObjectMeta: metav1.ObjectMeta{
146+
GenerateName: "bundle-image-",
147+
},
148+
})
149+
if err != nil {
150+
return nil, nil, err
151+
}
152+
153+
launchJob := batchv1.Job{
154+
ObjectMeta: metav1.ObjectMeta{
155+
GenerateName: "deploy-bundle-image-",
156+
},
157+
Spec: batchv1.JobSpec{
158+
//ttlSecondsAfterFinished: 0 // can use in the future to not have to clean up job
159+
Template: corev1.PodTemplateSpec{
160+
ObjectMeta: metav1.ObjectMeta{
161+
Name: "bundle-image",
162+
},
163+
Spec: corev1.PodSpec{
164+
RestartPolicy: corev1.RestartPolicyOnFailure,
165+
Containers: []corev1.Container{
166+
{
167+
Name: "bundle-image",
168+
Image: bundleImage,
169+
ImagePullPolicy: "Never",
170+
Command: []string{"/injected/opm", "alpha", "bundle", "extract", "-n", namespace, "-c", newConfigMap.GetName()},
171+
Env: []corev1.EnvVar{
172+
{
173+
Name: EnvContainerImage,
174+
Value: bundleImage,
175+
},
176+
},
177+
VolumeMounts: []corev1.VolumeMount{
178+
{
179+
Name: "copydir",
180+
MountPath: "/injected",
181+
},
182+
},
183+
},
184+
},
185+
InitContainers: []corev1.Container{
186+
{
187+
Name: "copy-binary",
188+
Image: initImage,
189+
ImagePullPolicy: "Never",
190+
Command: []string{"/bin/cp", "opm", "/copy-dest"},
191+
VolumeMounts: []corev1.VolumeMount{
192+
{
193+
Name: "copydir",
194+
MountPath: "/copy-dest",
195+
},
196+
},
197+
},
198+
},
199+
Volumes: []corev1.Volume{
200+
{
201+
Name: "copydir",
202+
VolumeSource: corev1.VolumeSource{
203+
EmptyDir: &corev1.EmptyDirVolumeSource{},
204+
},
205+
},
206+
},
207+
},
208+
},
209+
},
210+
}
211+
launchedJob, err := kubeclient.BatchV1().Jobs(namespace).Create(&launchJob)
212+
if err != nil {
213+
err := kubeclient.CoreV1().ConfigMaps(namespace).Delete(newConfigMap.GetName(), &metav1.DeleteOptions{})
214+
if err != nil {
215+
// already in an error, so just report it
216+
logrus.Errorf("failed to remove configmap: %v", err)
217+
}
218+
return nil, nil, err
219+
}
220+
221+
return newConfigMap, launchedJob, nil
222+
}

0 commit comments

Comments
 (0)