Skip to content

Commit

Permalink
cmd/operator-sdk/olmcatalog: scaffold package.yaml for a set of… (#1364)
Browse files Browse the repository at this point in the history
* cmd/operator-sdk/olmcatalog/gen-csv.go: add PackageManifest scaffold
and the optional --csv-channel and --default-channel flags to specify in
which channel an operator's CSV name should reside in their package manifest

* internal/pkg/scaffold/olm-catalog: PackageManifest scaffold adds and
updates an <operatorname>.package.yaml file in
deploy/olm-catalo/<operator-name>, a file used by OLM to manage operator CSV's

* Gopkg.lock,vendor: revendor

* doc/sdk-cli-reference.md: add --csv-channel and --default-channel to gen-csv

* CHANGELOG.md: add package manifest flags
  • Loading branch information
Eric Stroczynski authored Jun 28, 2019
1 parent eedbe31 commit 0fc5746
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Adds a new extra variable containing the unmodified CR spec for ansible based operators. [#1563](https://github.com/operator-framework/operator-sdk/pull/1563)
- New flag `--repo` for subcommands [`new`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#new) and [`migrate`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#migrate) specifies the repository path to be used in Go source files generated by the SDK. This flag can only be used with [Go modules](https://github.com/golang/go/wiki/Modules). ([#1475](https://github.com/operator-framework/operator-sdk/pull/1475))
- Adds `--go-build-args` flag to `operator-sdk build` for providing additional Go build arguments. ([#1582](https://github.com/operator-framework/operator-sdk/pull/1582))
- New flags `--csv-channel` and `--default-channel` for subcommand [`gen-csv`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#gen-csv) that add channels to and update the [package manifest](https://github.com/operator-framework/operator-registry/#manifest-format) in `deploy/olm-catalog/<operator-name>` when generating a new CSV or updating an existing one. ([#1364](https://github.com/operator-framework/operator-sdk/pull/1364))

### Changed

Expand Down
4 changes: 3 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions cmd/operator-sdk/olmcatalog/gen-csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ import (
)

var (
csvVersion string
fromVersion string
csvConfigPath string
updateCRDs bool
operatorName string
csvVersion string
csvChannel string
fromVersion string
csvConfigPath string
operatorName string
updateCRDs bool
defaultChannel bool
)

func newGenCSVCmd() *cobra.Command {
Expand All @@ -55,11 +57,15 @@ Configure CSV generation by writing a config file 'deploy/olm-catalog/csv-config
}

genCSVCmd.Flags().StringVar(&csvVersion, "csv-version", "", "Semantic version of the CSV")
genCSVCmd.MarkFlagRequired("csv-version")
if err := genCSVCmd.MarkFlagRequired("csv-version"); err != nil {
log.Fatalf("Failed to mark `csv-version` flag for `olm-catalog gen-csv` subcommand as required: %v", err)
}
genCSVCmd.Flags().StringVar(&fromVersion, "from-version", "", "Semantic version of an existing CSV to use as a base")
genCSVCmd.Flags().StringVar(&csvConfigPath, "csv-config", "", "Path to CSV config file. Defaults to deploy/olm-catalog/csv-config.yaml")
genCSVCmd.Flags().BoolVar(&updateCRDs, "update-crds", false, "Update CRD manifests in deploy/{operator-name}/{csv-version} the using latest API's")
genCSVCmd.Flags().StringVar(&operatorName, "operator-name", "", "Operator name to use while generating CSV")
genCSVCmd.Flags().StringVar(&csvChannel, "csv-channel", "", "Channel the CSV should be registered under in the package manifest")
genCSVCmd.Flags().BoolVar(&defaultChannel, "default-channel", false, "Use the channel passed to --csv-channel as the package manifests' default channel. Only valid when --csv-channel is set")

return genCSVCmd
}
Expand Down Expand Up @@ -95,7 +101,15 @@ func genCSVFunc(cmd *cobra.Command, args []string) error {
ConfigFilePath: csvConfigPath,
OperatorName: operatorName,
}
if err := s.Execute(cfg, csv); err != nil {
err := s.Execute(cfg,
csv,
&catalog.PackageManifest{
CSVVersion: csvVersion,
Channel: csvChannel,
ChannelIsDefault: defaultChannel,
},
)
if err != nil {
return fmt.Errorf("catalog scaffold failed: (%v)", err)
}

Expand Down Expand Up @@ -130,6 +144,11 @@ func verifyGenCSVFlags() error {
if fromVersion != "" && csvVersion == fromVersion {
return fmt.Errorf("from-version (%s) cannot equal csv-version; set only csv-version instead", fromVersion)
}

if defaultChannel && csvChannel == "" {
return fmt.Errorf("default-channel can only be used if csv-channel is set")
}

return nil
}

Expand Down
2 changes: 2 additions & 0 deletions doc/sdk-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ Writes a Cluster Service Version (CSV) manifest and optionally CRD files to `dep
* `--from-version` string - Semantic version of CSV manifest to use as a base for a new version.
* `--csv-config` string - Path to CSV config file. Defaults to deploy/olm-catalog/csv-config.yaml.
* `--update-crds` Update CRD manifests in deploy/{operator-name}/{csv-version} using the latest CRD manifests.
* `--csv-channel` string - Channel the CSV should be registered under in the package manifest
* `--default-channel` - Use the channel passed to --csv-channel as the package manifests' default channel. Only valid when --csv-channel is set.

#### Example

Expand Down
1 change: 0 additions & 1 deletion internal/pkg/scaffold/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,5 @@ const (
BuildBinDir = BuildDir + filePathSep + "_output" + filePathSep + "bin"
BuildScriptDir = BuildDir + filePathSep + "bin"
DeployDir = "deploy"
OLMCatalogDir = DeployDir + filePathSep + "olm-catalog"
CRDsDir = DeployDir + filePathSep + "crds"
)
2 changes: 2 additions & 0 deletions internal/pkg/scaffold/olm-catalog/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (

// CSVConfig is a configuration file for CSV composition. Its fields contain
// file path information.
// TODO(estroz): define field for path to write CSV bundle.
// TODO(estroz): make CSVConfig a viper.Config
type CSVConfig struct {
// The operator manifest file path. Defaults to deploy/operator.yaml.
OperatorPath string `json:"operator-path,omitempty"`
Expand Down
7 changes: 4 additions & 3 deletions internal/pkg/scaffold/olm-catalog/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
)

const (
OLMCatalogDir = scaffold.DeployDir + string(filepath.Separator) + "olm-catalog"
CSVYamlFileExt = ".clusterserviceversion.yaml"
CSVConfigYamlFile = "csv-config.yaml"
)
Expand Down Expand Up @@ -85,14 +86,14 @@ func (s *CSV) GetInput() (input.Input, error) {
// Path is what the operator-registry expects:
// {manifests -> olm-catalog}/{operator_name}/{semver}/{operator_name}.v{semver}.clusterserviceversion.yaml
s.Path = filepath.Join(s.pathPrefix,
scaffold.OLMCatalogDir,
OLMCatalogDir,
operatorName,
s.CSVVersion,
getCSVFileName(operatorName, s.CSVVersion),
)
}
if s.ConfigFilePath == "" {
s.ConfigFilePath = filepath.Join(s.pathPrefix, scaffold.OLMCatalogDir, CSVConfigYamlFile)
s.ConfigFilePath = filepath.Join(s.pathPrefix, OLMCatalogDir, CSVConfigYamlFile)
}
return s.Input, nil
}
Expand Down Expand Up @@ -186,7 +187,7 @@ func getCSVFileName(name, version string) string {
func (s *CSV) getCSVPath(ver string) string {
lowerProjName := strings.ToLower(s.OperatorName)
name := getCSVFileName(lowerProjName, ver)
return filepath.Join(s.pathPrefix, scaffold.OLMCatalogDir, lowerProjName, ver, name)
return filepath.Join(s.pathPrefix, OLMCatalogDir, lowerProjName, ver, name)
}

// getDisplayName turns a project dir name in any of {snake, chain, camel}
Expand Down
190 changes: 190 additions & 0 deletions internal/pkg/scaffold/olm-catalog/package_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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 catalog

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"

"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"

"github.com/ghodss/yaml"
olmregistry "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
)

const PackageManifestFileExt = ".package.yaml"

type PackageManifest struct {
input.Input

// CSVVersion is the version of the CSV being updated.
CSVVersion string
// Channel is CSVVersion's package manifest channel. If a new package
// manifest is generated, this channel will be the manifest default.
Channel string
// If ChannelIsDefault is true, Channel will be the package manifests'
// default channel.
ChannelIsDefault bool
}

var _ input.File = &PackageManifest{}

// GetInput gets s' Input.
func (s *PackageManifest) GetInput() (input.Input, error) {
if s.Path == "" {
lowerProjName := strings.ToLower(s.ProjectName)
// Path is what the operator-registry expects:
// {manifests -> olm-catalog}/{operator_name}/{operator_name}.package.yaml
s.Path = filepath.Join(OLMCatalogDir, lowerProjName,
lowerProjName+PackageManifestFileExt)
}
return s.Input, nil
}

var _ scaffold.CustomRenderer = &PackageManifest{}

// SetFS is a no-op to implement CustomRenderer.
func (s *PackageManifest) SetFS(_ afero.Fs) {}

// CustomRender either reads an existing package manifest or creates a new
// manifest and modifies it based on values set in s.
func (s *PackageManifest) CustomRender() ([]byte, error) {
i, err := s.GetInput()
if err != nil {
return nil, err
}
path := filepath.Join(s.AbsProjectPath, i.Path)

pm := &olmregistry.PackageManifest{}
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
pm = s.newPackageManifest()
} else if err == nil {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to read package manifest %s", path)
}
if len(b) > 0 {
if err = yaml.Unmarshal(b, pm); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal package manifest %s", path)
}
} else {
// File exists but is empty.
pm = s.newPackageManifest()
}
} else {
return nil, errors.Wrapf(err, "package manifest %s", path)
}

if err := validatePackageManifest(pm); err != nil {
return nil, errors.Wrapf(err, "failed to validate package manifest %s", pm.PackageName)
}

if err = s.setChannels(pm); err != nil {
return nil, err
}

sort.Slice(pm.Channels, func(i int, j int) bool {
return pm.Channels[i].Name < pm.Channels[j].Name
})

return yaml.Marshal(pm)
}

func (s *PackageManifest) newPackageManifest() *olmregistry.PackageManifest {
// Take the current CSV version to be the "alpha" channel, as an operator
// should only be designated anything more stable than "alpha" by a human.
channel := "alpha"
if s.Channel != "" {
channel = s.Channel
}
pm := &olmregistry.PackageManifest{
PackageName: s.ProjectName,
Channels: []olmregistry.PackageChannel{
{Name: channel, CurrentCSVName: getCSVName(s.ProjectName, s.CSVVersion)},
},
DefaultChannelName: channel,
}
return pm
}

func validatePackageManifest(pm *olmregistry.PackageManifest) error {
if pm.PackageName == "" {
return fmt.Errorf("package name cannot be empty")
}
if len(pm.Channels) == 0 {
return fmt.Errorf("channels cannot be empty")
}
if pm.DefaultChannelName == "" {
return fmt.Errorf("default channel cannot be empty")
}

seen := map[string]struct{}{}
for i, c := range pm.Channels {
if c.Name == "" {
return fmt.Errorf("channel %d name cannot be empty", i)
}
if c.CurrentCSVName == "" {
return fmt.Errorf("channel %s currentCSV cannot be empty", c.Name)
}
if _, ok := seen[c.Name]; ok {
return fmt.Errorf("duplicate package manifest channel name %s; channel names must be unique", c.Name)
}
seen[c.Name] = struct{}{}
}
if _, ok := seen[pm.DefaultChannelName]; !ok {
return fmt.Errorf("default channel %s does not exist in channels", pm.DefaultChannelName)
}

return nil
}

// setChannels checks for duplicate channels in pm and sets the default
// channel if possible.
func (s *PackageManifest) setChannels(pm *olmregistry.PackageManifest) error {
if s.Channel != "" {
pm.Channels = append(pm.Channels, olmregistry.PackageChannel{
Name: s.Channel,
CurrentCSVName: getCSVName(s.ProjectName, s.CSVVersion),
})
}

// Use s.Channel as the default channel if caller has specified it as the
// default.
if s.ChannelIsDefault && s.Channel != "" {
pm.DefaultChannelName = s.Channel
}
if pm.DefaultChannelName == "" {
log.Warn("Package manifest default channel is empty and should be set to an existing channel.")
}
defaultExists := false
for _, c := range pm.Channels {
if pm.DefaultChannelName == c.Name {
defaultExists = true
}
}
if !defaultExists {
log.Warnf("Package manifest default channel %s does not exist in channels.", pm.DefaultChannelName)
}

return nil
}
Loading

0 comments on commit 0fc5746

Please sign in to comment.