Skip to content
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

[sdk-stamps] add sdk stamps to bundle images #3120

Merged
3 changes: 3 additions & 0 deletions changelog/fragments/add-sdk-stamps.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
entries:
- description: Add sdk annotations to bundle resources (CSVs, `annotations.yaml` and `bundle.dockerfile`).
kind: "addition"
57 changes: 56 additions & 1 deletion cmd/operator-sdk/bundle/create.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,8 @@ import (
catalog "github.com/operator-framework/operator-sdk/internal/generate/olm-catalog"
"github.com/operator-framework/operator-sdk/internal/util/projutil"

"github.com/operator-framework/operator-sdk/internal/registry"

"github.com/blang/semver"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
scorecard "github.com/operator-framework/operator-sdk/internal/scorecard/alpha"
@@ -234,7 +236,14 @@ func (c bundleCreateCmd) runGenerate() error {
if err != nil {
return fmt.Errorf("error generating bundle image files: %v", err)
}
if err = copyScorecardConfig(); err != nil {

// rootDir is the location where metadata directory is present.
rootDir := c.outputDir
if rootDir == "" {
rootDir = filepath.Dir(c.directory)
}

if err = RewriteBundleImageContents(rootDir); err != nil {
return err
}
return nil
@@ -330,3 +339,49 @@ func copyScorecardConfig() error {
}
return nil
}

func addLabelsToDockerfile(filename string, metricAnnotation map[string]string) error {
var sdkMetricContent strings.Builder
for key, value := range metricAnnotation {
sdkMetricContent.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value))
}

err := projutil.RewriteFileContents(filename, "LABEL", sdkMetricContent.String())
if err != nil {
return fmt.Errorf("error rewriting dockerfile with metric labels, %v", err)
}
return nil
}

func RewriteBundleImageContents(rootDir string) error {
metricLabels := projutil.MakeBundleMetricsLabels()

// write metric labels to bundle.Dockerfile
if err := addLabelsToDockerfile(bundle.DockerFile, metricLabels); err != nil {
return fmt.Errorf("error writing metric labels to bundle.dockerfile: %v", err)
}

annotationsFilePath := getAnnotationsFilePath(rootDir)
if err := addLabelsToAnnotations(annotationsFilePath, metricLabels); err != nil {
return fmt.Errorf("error writing metric labels to annotations.yaml: %v", err)
}

// Add a COPY for the scorecard config to bundle.Dockerfile.
if err := copyScorecardConfig(); err != nil {
return fmt.Errorf("error copying scorecardConfig to bundle image, %v", err)
}
return nil
}

// getAnnotationsFilePath return the locations of annotations.yaml.
func getAnnotationsFilePath(rootDir string) string {
return filepath.Join(rootDir, bundle.MetadataDir, bundle.AnnotationsFile)
}

func addLabelsToAnnotations(filename string, metricLables map[string]string) error {
err := registry.RewriteAnnotationsYaml(filename, metricLables)
if err != nil {
return err
}
return nil
}
31 changes: 31 additions & 0 deletions cmd/operator-sdk/generate/bundle/bundle.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import (
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
"sigs.k8s.io/kubebuilder/pkg/model/config"

genbundle "github.com/operator-framework/operator-sdk/cmd/operator-sdk/bundle"
genutil "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate/internal"
gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion"
"github.com/operator-framework/operator-sdk/internal/generate/collector"
@@ -255,9 +256,39 @@ func (c bundleCmd) runMetadata() error {

// generateMetadata wraps the operator-registry bundle Dockerfile/metadata generator.
func (c bundleCmd) generateMetadata(manifestsDir, outputDir string) error {

metadataExists := checkMetatdataExists(outputDir, manifestsDir)
err := bundle.GenerateFunc(manifestsDir, outputDir, c.operatorName, c.channels, c.defaultChannel, c.overwrite)
if err != nil {
return fmt.Errorf("error generating bundle metadata: %v", err)
}

// Add SDK stamps if metadata is not present before or when overwrite is set to true.
if c.overwrite || !metadataExists {
rootDir := outputDir
if rootDir == "" {
rootDir = filepath.Dir(manifestsDir)
}

if err = genbundle.RewriteBundleImageContents(rootDir); err != nil {
return err
}
}
return nil
}

// checkMetatdataExists returns true if bundle.Dockerfile and metadataDir exist, if not
// it returns false.
func checkMetatdataExists(outputDir, manifestsDir string) bool {
var annotationsDir string
if outputDir == "" {
annotationsDir = filepath.Dir(manifestsDir) + bundle.MetadataDir
} else {
annotationsDir = outputDir + bundle.MetadataDir
}

if genutil.IsNotExist(bundle.DockerFile) || genutil.IsNotExist(annotationsDir) {
Copy link
Member

Choose a reason for hiding this comment

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

To make this check robust, you need to check and write to each file individually, since one may exist without the other.

Copy link
Member Author

@varshaprasad96 varshaprasad96 Jun 30, 2020

Choose a reason for hiding this comment

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

Yes, but I was thinking of rewriting both of them even if one exists, because until tweaked manually GenerateFunc of OR is going to write both and also generate bundle provides a --metadata option which writes both. In the sense, if there is a scenario where only one of them exists, doesn't it mean that either it has been manually deleted or there is some error (though I assume its taken care of in respective commands create/generate) and wouldn't rewriting both of them together in a clean state be a better option in such a scenario?

return false
}
return true
}
20 changes: 20 additions & 0 deletions internal/generate/clusterserviceversion/clusterserviceversion.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import (
"strings"

"github.com/blang/semver"
"github.com/operator-framework/api/pkg/operators/v1alpha1"
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -160,13 +161,29 @@ func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) {
return err
}

// Add sdk labels to csv
setSDKAnnotations(csv)

w, err := g.getWriter()
if err != nil {
return err
}
return genutil.WriteObject(w, csv)
}

// setSDKAnnotations adds SDK metric labels to the base if they do not exist.
func setSDKAnnotations(csv *v1alpha1.ClusterServiceVersion) {
annotations := csv.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}

for key, value := range projutil.MakeOperatorMetricLabels() {
annotations[key] = value
}
csv.SetAnnotations(annotations)
}

// LegacyOption is a function that modifies a Generator for legacy project layouts.
type LegacyOption Option

@@ -204,6 +221,9 @@ func (g *Generator) GenerateLegacy(opts ...LegacyOption) (err error) {
return err
}

// Add sdk labels to csv
setSDKAnnotations(csv)

w, err := g.getWriter()
if err != nil {
return err
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -59,6 +60,11 @@ var (
cfg *config.Config
)

const (
testSDKbuilderStamp = "operators.operatorframework.io/builder: operator-sdk-unknown"
testSDKlayoutStamp = "operators.operatorframework.io/project_layout: unknown"
)

var (
baseCSV, baseCSVUIMeta, newCSV *v1alpha1.ClusterServiceVersion
baseCSVStr, baseCSVUIMetaStr, newCSVStr string
@@ -121,7 +127,8 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
WithWriter(buf),
}
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
Expect(buf.String()).To(MatchYAML(newCSVStr))
outputCSV := removeSDKLabelsFromCSVString(buf.String())
Expect(outputCSV).To(MatchYAML(newCSVStr))
})
It("should write a ClusterServiceVersion manifest to a base file", func() {
g = Generator{
@@ -135,7 +142,27 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
outputFile := filepath.Join(tmp, "bases", makeCSVFileName(operatorName))
Expect(outputFile).To(BeAnExistingFile())
Expect(string(readFileHelper(outputFile))).To(MatchYAML(baseCSVUIMetaStr))
Expect(readFileHelper(outputFile)).To(MatchYAML(baseCSVUIMetaStr))
})
It("should have sdk labels in annotations", func() {
g = Generator{
OperatorName: operatorName,
OperatorType: operatorType,
}
opts := []Option{
WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff),
WithBaseWriter(tmp),
}
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
outputFile := filepath.Join(tmp, "bases", makeCSVFileName(operatorName))
outputCSV, _, err := getCSVFromFile(outputFile)
Expect(err).ToNot(HaveOccurred())
Expect(outputFile).To(BeAnExistingFile())

annotations := outputCSV.GetAnnotations()
Expect(annotations).ToNot(BeNil())
Expect(annotations).Should(HaveKey(projutil.OperatorBuilder))
Expect(annotations).Should(HaveKey(projutil.OperatorLayout))
})
It("should write a ClusterServiceVersion manifest to a bundle file", func() {
g = Generator{
@@ -151,7 +178,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName))
Expect(outputFile).To(BeAnExistingFile())
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
})
It("should write a ClusterServiceVersion manifest to a package file", func() {
g = Generator{
@@ -167,7 +194,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
outputFile := filepath.Join(tmp, g.Version, makeCSVFileName(operatorName))
Expect(outputFile).To(BeAnExistingFile())
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
})

It("should write a ClusterServiceVersion manifest to a legacy bundle file", func() {
@@ -184,7 +211,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
Expect(g.GenerateLegacy(opts...)).ToNot(HaveOccurred())
outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName))
Expect(outputFile).To(BeAnExistingFile())
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
})
It("should write a ClusterServiceVersion manifest as a legacy package file", func() {
g = Generator{
@@ -332,7 +359,6 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
Expect(csv).To(Equal(upgradeCSV(newCSV, g.OperatorName, g.Version)))
})
})

})

})
@@ -397,10 +423,10 @@ func initTestCSVsHelper() {
ExpectWithOffset(1, err).ToNot(HaveOccurred())
}

func readFileHelper(path string) []byte {
func readFileHelper(path string) string {
b, err := ioutil.ReadFile(path)
ExpectWithOffset(1, err).ToNot(HaveOccurred())
return b
return removeSDKLabelsFromCSVString(string(b))
}

func modifyCSVDepImageHelper(tag string) func(csv *v1alpha1.ClusterServiceVersion) {
@@ -469,3 +495,11 @@ func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1al

return upgraded
}

// removeSDKLabelsFromCSVString to remove the sdk labels from test CSV structs. The test
// cases do not generate a PROJECTFILE or an entire operator to get the version or layout
// of SDK. Hence the values of those will appear "unknown".
func removeSDKLabelsFromCSVString(csv string) string {
replacer := strings.NewReplacer(testSDKbuilderStamp, "", testSDKlayoutStamp, "")
return replacer.Replace(csv)
}
60 changes: 60 additions & 0 deletions internal/registry/validate.go
Original file line number Diff line number Diff line change
@@ -16,14 +16,18 @@ package registry

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

apimanifests "github.com/operator-framework/api/pkg/manifests"
apivalidation "github.com/operator-framework/api/pkg/validation"
apierrors "github.com/operator-framework/api/pkg/validation/errors"
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"gopkg.in/yaml.v2"
"sigs.k8s.io/yaml"

Copy link
Member Author

@varshaprasad96 varshaprasad96 Jun 6, 2020

Choose a reason for hiding this comment

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

Switching over to gopkg.in/yaml.v2. k8s-yaml interprets and appends the name of the field defined in yaml struct while marshaling and unmarshaling, whereas gopkg-yaml uses the yaml annotation defined in the struct. Example - https://play.golang.org/p/uQ_J4oZBbCR

Copy link
Contributor

Choose a reason for hiding this comment

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

We are trying to remove the "gopkg.in/yaml.v2". See : #2790
So, we should not add new code with it.

Copy link
Member Author

Choose a reason for hiding this comment

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

The operator-registry is still using gopkg.in/yaml.v2 for generation of annotation.yaml. Here: https://github.com/operator-framework/operator-registry/blob/master/pkg/lib/bundle/generate.go#L12. And this function is trying to essentially replicate their process.

This comment was marked as outdated.

Copy link
Member Author

@varshaprasad96 varshaprasad96 Jun 11, 2020

Choose a reason for hiding this comment

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

The unit tests do pass with "sigs.k8s.io/yaml". However, it would fail in the scenario where we create bundle image using files already present on disk.
Explanation is as below:

To reproduce the scenario:

  1. First create bundle files on disk with operator-sdk bundle create command and --generate-only flag.
  2. Now create bundle image and push it to the registry (ie use the eg. command: operator-sdk bundle create quay.io/example/memcached-operator:latest --directory path/to/manifests --package memcached-operator)

The error which we get would be:
Unmatched number of fields. Expected 6 vs existing 0. It fails here.

Cause of error:
When we use sigs.k8s.io/yaml, the key in annotations.yaml file would be the name of the field in AnnotationMetadata struct. The annotations.yaml which would be rewritten after adding the sdk stamps, would be like this:

Annotations:
  operators.operatorframework.io.bundle.channel.default.v1: stable
  operators.operatorframework.io.bundle.channels.v1: stable
  operators.operatorframework.io.bundle.manifests.v1: manifests/

Now during unmarshalling, ie here, it would not be able to interpret this yaml because it has the key Annotations not annotations. The gopkg at operator-registry expects the key in yaml file to be annotations.

If we use "gopkg.in/yaml.v2", the annotations.yaml which we re-write, would take the marker value (name of field) defined in yaml struct, yaml:"annotations" as it does in operator-registry and it would unmarshall properly.

I have created a simple example to show the difference between both the packages in this playground: https://play.golang.org/p/uQ_J4oZBbCR

This scenario is being checked in one of the CI tests.

Copy link
Contributor

@camilamacedo86 camilamacedo86 Jun 11, 2020

Choose a reason for hiding this comment

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

See that your example will work if we replace the yaml for JSON

Something int yaml:"somethingGreat,omitempty"
With
Something int json:"somethingGreat,omitempty"

Then, see that also it will work with yaml if we replace the Unmarshal/Marchal for JSONToYAML/YAMLToJSON. See: https://play.golang.org/p/D2guhgb7cvc

In SDK, we read/update/delete the files and use the lib to verify them. In this way, no matter the dep/lib that we use to manipulate it, all should work fine if at the end we have the same result. Then, I understand that if we change the impl here to use the above it should work.

However, if you still just facing problems with. could you please add a comment with a //todo: replace it for "sigs.k8s.io/yaml". Operator-registry defines the annotations with yaml format?

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding //todo: because of the dependency on operator-registry. OR defines annotations.

Copy link
Member

@joelanford joelanford Jun 24, 2020

Choose a reason for hiding this comment

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

This could likely be fixed by adding json:"annotations" to the struct tag in the operator-registry repo.

So it would look like:

type AnnotationMetadata struct {
	Annotations map[string]string `yaml:"annotations" json:"annotations"``
}

Obviously this would require an upstream PR to be merged and for us to update our dependency to a new commit/version, so we shouldn't block this PR for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will continue with above and have an upstream PR in parallel. Ill have a follow-up PR to remove the TODO.

k8svalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/apimachinery/pkg/util/validation/field"
)

@@ -127,3 +131,59 @@ func appendResult(results []apierrors.ManifestResult, r apierrors.ManifestResult

return results
}

// RewriteAnnotationsYaml unmarshalls the specified yaml file, appends the content and
// converts it again to yaml.
func RewriteAnnotationsYaml(filename string, content map[string]string) error {

metadata, err := getAnnotationFileContents(filename)
if err != nil {
return err
}

// Append the contents to annotationsYaml
for key, val := range content {
metadata.Annotations[key] = val
}

err = writeAnnotationFile(filename, metadata)
if err != nil {
return err
}

return nil
}

func getAnnotationFileContents(filename string) (*registrybundle.AnnotationMetadata, error) {
f, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}

annotationsYaml := &registrybundle.AnnotationMetadata{}
if err := yaml.Unmarshal(f, annotationsYaml); err != nil {
return nil, fmt.Errorf("error parsing annotations file: %v", err)
}
return annotationsYaml, nil
}

func writeAnnotationFile(filename string, annotation *registrybundle.AnnotationMetadata) error {
// TODO: replace `gopkg.in/yaml.v2` with `sigs.k8s.io/yaml`. Operator registry
// defines annotations with yaml format (using gopkg-yaml) and k8s-yaml takes name
// of field in the struct as the value of key in yaml.
file, err := yaml.Marshal(annotation)
if err != nil {
return err
}

mode := os.FileMode(0666)
if info, err := os.Stat(filename); err == nil {
mode = info.Mode()
}

err = ioutil.WriteFile(filename, []byte(file), mode)
if err != nil {
return fmt.Errorf("error writing modified contents to annotations file, %v", err)
}
return nil
}
Loading