github.com/theishshah/operator-sdk@v0.6.0/pkg/scaffold/crd.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package scaffold
    16  
    17  import (
    18  	"fmt"
    19  	"io/ioutil"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/operator-framework/operator-sdk/internal/util/k8sutil"
    26  	"github.com/operator-framework/operator-sdk/pkg/scaffold/input"
    27  
    28  	"github.com/ghodss/yaml"
    29  	"github.com/spf13/afero"
    30  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	crdgenerator "sigs.k8s.io/controller-tools/pkg/crd/generator"
    33  )
    34  
    35  // CRD is the input needed to generate a deploy/crds/<group>_<version>_<kind>_crd.yaml file
    36  type CRD struct {
    37  	input.Input
    38  
    39  	// Resource defines the inputs for the new custom resource definition
    40  	Resource *Resource
    41  
    42  	// IsOperatorGo is true when the operator is written in Go.
    43  	IsOperatorGo bool
    44  }
    45  
    46  func (s *CRD) GetInput() (input.Input, error) {
    47  	if s.Path == "" {
    48  		fileName := fmt.Sprintf("%s_%s_%s_crd.yaml",
    49  			strings.ToLower(s.Resource.Group),
    50  			strings.ToLower(s.Resource.Version),
    51  			s.Resource.LowerKind)
    52  		s.Path = filepath.Join(CRDsDir, fileName)
    53  	}
    54  	initCache()
    55  	return s.Input, nil
    56  }
    57  
    58  type fsCache struct {
    59  	afero.Fs
    60  }
    61  
    62  func (c *fsCache) fileExists(path string) bool {
    63  	_, err := c.Stat(path)
    64  	return err == nil
    65  }
    66  
    67  var (
    68  	// Global cache so users can use new CRD structs.
    69  	cache *fsCache
    70  	once  sync.Once
    71  )
    72  
    73  func initCache() {
    74  	once.Do(func() {
    75  		cache = &fsCache{Fs: afero.NewMemMapFs()}
    76  	})
    77  }
    78  
    79  func (s *CRD) SetFS(_ afero.Fs) {}
    80  
    81  func (s *CRD) CustomRender() ([]byte, error) {
    82  	i, _ := s.GetInput()
    83  	// controller-tools generates crd file names with no _crd.yaml suffix:
    84  	// <group>_<version>_<kind>.yaml.
    85  	path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1)
    86  
    87  	// controller-tools' generators read and make crds for all apis in pkg/apis,
    88  	// so generate crds in a cached, in-memory fs to extract the data we need.
    89  	if s.IsOperatorGo && !cache.fileExists(path) {
    90  		g := &crdgenerator.Generator{
    91  			RootPath:          s.AbsProjectPath,
    92  			Domain:            strings.SplitN(s.Resource.FullGroup, ".", 2)[1],
    93  			OutputDir:         ".",
    94  			SkipMapValidation: false,
    95  			OutFs:             cache,
    96  		}
    97  		if err := g.ValidateAndInitFields(); err != nil {
    98  			return nil, err
    99  		}
   100  		if err := g.Do(); err != nil {
   101  			return nil, err
   102  		}
   103  	}
   104  
   105  	dstCRD := newCRDForResource(s.Resource)
   106  	// Get our generated crd's from the in-memory fs. If it doesn't exist in the
   107  	// fs, the corresponding API does not exist yet, so scaffold a fresh crd
   108  	// without a validation spec.
   109  	// If the crd exists in the fs, and a local crd exists, append the validation
   110  	// spec. If a local crd does not exist, use the generated crd.
   111  	if _, err := cache.Stat(path); err != nil && !os.IsNotExist(err) {
   112  		return nil, err
   113  	} else if err == nil {
   114  		b, err := afero.ReadFile(cache, path)
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  		dstCRD = &apiextv1beta1.CustomResourceDefinition{}
   119  		if err = yaml.Unmarshal(b, dstCRD); err != nil {
   120  			return nil, err
   121  		}
   122  		val := dstCRD.Spec.Validation.DeepCopy()
   123  
   124  		// If the crd exists at i.Path, append the validation spec to its crd spec.
   125  		if _, err := os.Stat(i.Path); err == nil {
   126  			cb, err := ioutil.ReadFile(i.Path)
   127  			if err != nil {
   128  				return nil, err
   129  			}
   130  			if len(cb) > 0 {
   131  				dstCRD = &apiextv1beta1.CustomResourceDefinition{}
   132  				if err = yaml.Unmarshal(cb, dstCRD); err != nil {
   133  					return nil, err
   134  				}
   135  				dstCRD.Spec.Validation = val
   136  			}
   137  		}
   138  		// controller-tools does not set ListKind or Singular names.
   139  		dstCRD.Spec.Names = getCRDNamesForResource(s.Resource)
   140  		// Remove controller-tools default label.
   141  		delete(dstCRD.Labels, "controller-tools.k8s.io")
   142  	}
   143  	addCRDSubresource(dstCRD)
   144  	addCRDVersions(dstCRD)
   145  	return k8sutil.GetObjectBytes(dstCRD)
   146  }
   147  
   148  func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition {
   149  	return &apiextv1beta1.CustomResourceDefinition{
   150  		TypeMeta: metav1.TypeMeta{
   151  			APIVersion: "apiextensions.k8s.io/v1beta1",
   152  			Kind:       "CustomResourceDefinition",
   153  		},
   154  		ObjectMeta: metav1.ObjectMeta{
   155  			Name: r.Resource + "." + r.FullGroup,
   156  		},
   157  		Spec: apiextv1beta1.CustomResourceDefinitionSpec{
   158  			Group:   r.FullGroup,
   159  			Names:   getCRDNamesForResource(r),
   160  			Scope:   apiextv1beta1.NamespaceScoped,
   161  			Version: r.Version,
   162  			Subresources: &apiextv1beta1.CustomResourceSubresources{
   163  				Status: &apiextv1beta1.CustomResourceSubresourceStatus{},
   164  			},
   165  		},
   166  	}
   167  }
   168  
   169  func getCRDNamesForResource(r *Resource) apiextv1beta1.CustomResourceDefinitionNames {
   170  	return apiextv1beta1.CustomResourceDefinitionNames{
   171  		Kind:     r.Kind,
   172  		ListKind: r.Kind + "List",
   173  		Plural:   r.Resource,
   174  		Singular: r.LowerKind,
   175  	}
   176  }
   177  
   178  func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) {
   179  	if crd.Spec.Subresources == nil {
   180  		crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{}
   181  	}
   182  	if crd.Spec.Subresources.Status == nil {
   183  		crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{}
   184  	}
   185  }
   186  
   187  func addCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) {
   188  	// crd.Version is deprecated, use crd.Versions instead.
   189  	var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion
   190  	if crd.Spec.Version != "" {
   191  		var verExists, hasStorageVer bool
   192  		for _, ver := range crd.Spec.Versions {
   193  			if crd.Spec.Version == ver.Name {
   194  				verExists = true
   195  			}
   196  			// There must be exactly one version flagged as a storage version.
   197  			if ver.Storage {
   198  				hasStorageVer = true
   199  			}
   200  		}
   201  		if !verExists {
   202  			crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{
   203  				{Name: crd.Spec.Version, Served: true, Storage: !hasStorageVer},
   204  			}
   205  		}
   206  	} else {
   207  		crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{
   208  			{Name: "v1alpha1", Served: true, Storage: true},
   209  		}
   210  	}
   211  
   212  	if len(crd.Spec.Versions) > 0 {
   213  		// crd.Version should always be the first element in crd.Versions.
   214  		crd.Spec.Versions = append(crdVersions, crd.Spec.Versions...)
   215  	} else {
   216  		crd.Spec.Versions = crdVersions
   217  	}
   218  }