Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion cmd/operator-sdk/test/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package test

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -307,7 +308,7 @@ func replaceImage(manifestPath, image string) error {
}
foundDeployment := false
newManifest := []byte{}
scanner := internalk8sutil.NewYAMLScanner(yamlFile)
scanner := internalk8sutil.NewYAMLScanner(bytes.NewBuffer(yamlFile))
for scanner.Scan() {
yamlSpec := scanner.Bytes()

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
github.com/mattn/go-isatty v0.0.12
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.1.2
github.com/onsi/ginkgo v1.12.0
github.com/onsi/gomega v1.9.0
github.com/operator-framework/api v0.3.5
github.com/operator-framework/operator-registry v1.12.2
github.com/pborman/uuid v1.2.0
Expand Down
199 changes: 199 additions & 0 deletions internal/generate/clusterserviceversion/bases/clusterserviceversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright 2020 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 bases

import (
"bytes"
"fmt"
"io/ioutil"

"github.com/operator-framework/api/pkg/operators/v1alpha1"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/yaml"

"github.com/operator-framework/operator-sdk/internal/util/k8sutil"
"github.com/operator-framework/operator-sdk/internal/util/projutil"
)

// ClusterServiceVersion configures the v1alpha1.ClusterServiceVersion
// that GetBase() returns.
type ClusterServiceVersion struct {
// BasePath is the path to the base being read. If empty, GetBase() returns
// a default base.
BasePath string
// OperatorName is the operator's name, ex. app-operator
OperatorName string
// OperatorType
OperatorType projutil.OperatorType
// APIsDir contains project API definition files.
APIsDir string
// GVKs are all GroupVersionKinds in the project.
GVKs []schema.GroupVersionKind
// Interactive turns on an interactive prompt.
Interactive bool

// Fields for input to the base.
DisplayName string
Description string
Maturity string
Capabilities string
Keywords []string
Provider v1alpha1.AppLink
Links []v1alpha1.AppLink
Maintainers []v1alpha1.Maintainer
Icon []v1alpha1.Icon // TODO(estroz): read icon bytes from files.
}

// GetBase returns a base v1alpha1.ClusterServiceVersion, populated
// either with default values or, if b.BasePath is set, bytes from disk.
func (b ClusterServiceVersion) GetBase() (base *v1alpha1.ClusterServiceVersion, err error) {
if b.BasePath != "" {
if base, err = readClusterServiceVersionBase(b.BasePath); err != nil {
return nil, fmt.Errorf("error reading existing ClusterServiceVersion base %s: %v", b.BasePath, err)
}
} else {
b.setDefaults()
base = b.makeNewBase()
}

// Interactively fill in UI metadata.
if b.Interactive {
meta := &uiMetadata{}
meta.runInteractivePrompt()
meta.apply(base)
}

if b.APIsDir != "" {
switch b.OperatorType {
case projutil.OperatorTypeGo:
if err := updateDescriptionsForGVKs(base, b.APIsDir, b.GVKs); err != nil {
return nil, fmt.Errorf("error generating ClusterServiceVersion base metadata: %w", err)
}
}
}

return base, nil
}

// setDefaults sets default values in b using b's existing values.
func (b *ClusterServiceVersion) setDefaults() {
if b.DisplayName == "" {
b.DisplayName = k8sutil.GetDisplayName(b.OperatorName)
}
if b.Description == "" {
b.Description = b.DisplayName + " description. TODO."
}
if b.Maturity == "" {
b.Maturity = "alpha"
}
if b.Capabilities == "" {
b.Capabilities = "Basic Install"
}
if len(b.Keywords) == 0 || b.Keywords[0] == "" {
b.Keywords = []string{b.OperatorName}
}
if len(b.Links) == 0 || b.Links[0] == (v1alpha1.AppLink{}) {
b.Links = []v1alpha1.AppLink{
{
Name: b.DisplayName,
URL: fmt.Sprintf("https://%s.domain", b.OperatorName),
},
}
}
if len(b.Icon) == 0 {
b.Icon = make([]v1alpha1.Icon, 1)
}
if b.Provider == (v1alpha1.AppLink{}) {
b.Provider = v1alpha1.AppLink{
Name: "Provider Name",
URL: "https://your.domain",
}
}
if len(b.Maintainers) == 0 || b.Maintainers[0] == (v1alpha1.Maintainer{}) {
b.Maintainers = []v1alpha1.Maintainer{
{
Name: "Maintainer Name",
Email: "your@email.com",
},
}
}
}

// makeNewBase returns a base v1alpha1.ClusterServiceVersion to modify.
func (b ClusterServiceVersion) makeNewBase() *v1alpha1.ClusterServiceVersion {
return &v1alpha1.ClusterServiceVersion{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.ClusterServiceVersionAPIVersion,
Kind: v1alpha1.ClusterServiceVersionKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: b.OperatorName + ".vX.Y.Z",
Namespace: "placeholder",
Annotations: map[string]string{
"capabilities": b.Capabilities,
"alm-examples": "[]",
},
},
Spec: v1alpha1.ClusterServiceVersionSpec{
DisplayName: b.DisplayName,
Description: b.Description,
Provider: b.Provider,
Maintainers: b.Maintainers,
Links: b.Links,
Maturity: b.Maturity,
Keywords: b.Keywords,
Icon: b.Icon,
InstallModes: []v1alpha1.InstallMode{
{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true},
{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true},
{Type: v1alpha1.InstallModeTypeMultiNamespace, Supported: false},
{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true},
},
},
}
}

// readClusterServiceVersionBase returns the ClusterServiceVersion base at path.
// If no base is found, readClusterServiceVersionBase returns an error.
func readClusterServiceVersionBase(path string) (*v1alpha1.ClusterServiceVersion, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}

scanner := k8sutil.NewYAMLScanner(bytes.NewBuffer(b))
for scanner.Scan() {
manifest := scanner.Bytes()
typeMeta, err := k8sutil.GetTypeMetaFromBytes(manifest)
if err != nil {
log.Debugf("Skipping non-Object manifest %s: %v", path, err)
continue
}
if typeMeta.Kind == v1alpha1.ClusterServiceVersionKind {
csv := &v1alpha1.ClusterServiceVersion{}
if err := yaml.Unmarshal(manifest, csv); err != nil {
return nil, fmt.Errorf("error unmarshalling ClusterServiceVersion from manifest %s: %v", path, err)
}
return csv, nil
}
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning manifest %s: %v", path, err)
}

return nil, fmt.Errorf("no ClusterServiceVersion manifest in %s", path)
}
75 changes: 75 additions & 0 deletions internal/generate/clusterserviceversion/bases/markers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2020 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 bases

import (
"errors"
"fmt"
"strings"

"github.com/markbates/inflect"
"github.com/operator-framework/api/pkg/operators/v1alpha1"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/operator-framework/operator-sdk/internal/generate/olm-catalog/descriptor"
)

// updateDescriptionsForGVKs updates csv with API metadata found in apisDir
// filtered by gvks.
func updateDescriptionsForGVKs(csv *v1alpha1.ClusterServiceVersion, apisDir string,
gvks []schema.GroupVersionKind) error {

gvkMap := make(map[schema.GroupVersionKind]v1alpha1.CRDDescription)
for _, desc := range csv.Spec.CustomResourceDefinitions.Owned {
group := desc.Name
if split := strings.Split(desc.Name, "."); len(split) > 1 {
group = strings.Join(split[1:], ".")
}
// Parse CRD descriptors from source code comments and annotations.
gvk := schema.GroupVersionKind{
Group: group,
Version: desc.Version,
Kind: desc.Kind,
}
gvkMap[gvk] = desc
}

descriptions := []v1alpha1.CRDDescription{}
for _, gvk := range gvks {
newDescription, err := descriptor.GetCRDDescriptionForGVK(apisDir, gvk)
if err != nil {
if errors.Is(err, descriptor.ErrAPIDirNotExist) {
log.Debugf("Directory for API %s does not exist. Skipping CSV annotation parsing for API.", gvk)
} else if errors.Is(err, descriptor.ErrAPITypeNotFound) {
log.Debugf("No kind type found for API %s. Skipping CSV annotation parsing for API.", gvk)
} else {
// TODO: Should we ignore all CSV annotation parsing errors and simply log the error
// like we do for the above cases.
return fmt.Errorf("failed to set CRD descriptors for %s: %v", gvk, err)
}
// Keep the existing description and don't update on error
if desc, hasDesc := gvkMap[gvk]; hasDesc {
descriptions = append(descriptions, desc)
}
} else {
// Replace the existing description with the newly parsed one
newDescription.Name = inflect.Pluralize(strings.ToLower(gvk.Kind)) + "." + gvk.Group
descriptions = append(descriptions, newDescription)
}
}
csv.Spec.CustomResourceDefinitions.Owned = descriptions
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,48 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package olmcatalog
package bases

import (
"strings"

olmapiv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/api/pkg/operators/v1alpha1"

"github.com/operator-framework/operator-sdk/internal/util/projutil"
)

// InteractiveCSVCmd includes the list of CSV fields which would be asked
// uiMetadata includes the list of CSV fields which would be asked
// to the user while generating CSV.
type interactiveCSVCmd struct {
type uiMetadata struct {
// DisplayName is the name of the crd.
DisplayName string
// Keyword is a list of keywords describing the operator.
Keywords []string
// Description of the operator. Can include the features, limitations or
// use-cases of the operator.
Description string
// Name of the publishing entity behind the operator.
ProviderName string
// URL related to the publishing entity behind the operator.
ProviderURL string
// Keyword is a list of keywords describing the operator.
Keywords []string
// Maintainers is the list of organizational entities maintaining the operator.
Maintainers []string
// FEAT: read icon bytes from files.
}

// generateInteractivePrompt generates the prompts for user to provide input to the CSV
// fields.
func (s *interactiveCSVCmd) generateInteractivePrompt() {
// runInteractivePrompt prompts the user to provide input to uiMetadata fields.
func (s *uiMetadata) runInteractivePrompt() {
s.DisplayName = projutil.GetRequiredInput("Display name for the operator")
s.Keywords = projutil.GetStringArray("Comma-separated list of keywords for your operator")
s.Description = projutil.GetRequiredInput("Description for the operator")
s.ProviderName = projutil.GetRequiredInput("Provider's name for the operator")
s.ProviderURL = projutil.GetOptionalInput("Any relevant URL for the provider name")
s.Keywords = projutil.GetStringArray("Comma-separated list of keywords for your operator")
s.Maintainers = projutil.GetStringArray("Comma-separated list of maintainers and their emails" +
" (e.g. 'name1:email1, name2:email2')")
}

// addUImetadata populates the CSV with the data obtained from the interactive
// prompts which appear while generating CSV.
func (s *interactiveCSVCmd) addUImetadata(csv *olmapiv1alpha1.ClusterServiceVersion) {
// apply populates the CSV with the data in s.
func (s uiMetadata) apply(csv *v1alpha1.ClusterServiceVersion) {
if s.DisplayName != "" {
csv.Spec.DisplayName = s.DisplayName
}
Expand All @@ -67,11 +67,11 @@ func (s *interactiveCSVCmd) addUImetadata(csv *olmapiv1alpha1.ClusterServiceVers
}

if len(s.Maintainers) != 0 {
maintainers := make([]olmapiv1alpha1.Maintainer, 0)
maintainers := make([]v1alpha1.Maintainer, 0)
for _, entity := range s.Maintainers {
entityDetails := strings.Split(entity, ":")
if len(entityDetails) == 2 {
m := olmapiv1alpha1.Maintainer{}
m := v1alpha1.Maintainer{}
m.Name, m.Email = entityDetails[0], entityDetails[1]
maintainers = append(maintainers, m)
}
Expand All @@ -80,12 +80,11 @@ func (s *interactiveCSVCmd) addUImetadata(csv *olmapiv1alpha1.ClusterServiceVers
}

if s.ProviderName != "" {
provider := olmapiv1alpha1.AppLink{}
provider := v1alpha1.AppLink{}
provider.Name = s.ProviderName
if s.ProviderURL != "" {
provider.URL = s.ProviderURL
}
csv.Spec.Provider = provider
}

}
Loading