github.com/jmrodri/operator-sdk@v0.5.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/pkg/scaffold/input"
    26  
    27  	"github.com/ghodss/yaml"
    28  	"github.com/spf13/afero"
    29  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    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) CustomRender() ([]byte, error) {
    80  	i, _ := s.GetInput()
    81  	// controller-tools generates crd file names with no _crd.yaml suffix:
    82  	// <group>_<version>_<kind>.yaml.
    83  	path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1)
    84  
    85  	// controller-tools' generators read and make crds for all apis in pkg/apis,
    86  	// so generate crds in a cached, in-memory fs to extract the data we need.
    87  	if s.IsOperatorGo && !cache.fileExists(path) {
    88  		g := &crdgenerator.Generator{
    89  			RootPath:          s.AbsProjectPath,
    90  			Domain:            strings.SplitN(s.Resource.FullGroup, ".", 2)[1],
    91  			OutputDir:         ".",
    92  			SkipMapValidation: false,
    93  			OutFs:             cache,
    94  		}
    95  		if err := g.ValidateAndInitFields(); err != nil {
    96  			return nil, err
    97  		}
    98  		if err := g.Do(); err != nil {
    99  			return nil, err
   100  		}
   101  	}
   102  
   103  	dstCRD := newCRDForResource(s.Resource)
   104  	// Get our generated crd's from the in-memory fs. If it doesn't exist in the
   105  	// fs, the corresponding API does not exist yet, so scaffold a fresh crd
   106  	// without a validation spec.
   107  	// If the crd exists in the fs, and a local crd exists, append the validation
   108  	// spec. If a local crd does not exist, use the generated crd.
   109  	if _, err := cache.Stat(path); err != nil && !os.IsNotExist(err) {
   110  		return nil, err
   111  	} else if err == nil {
   112  		b, err := afero.ReadFile(cache, path)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		dstCRD = &apiextv1beta1.CustomResourceDefinition{}
   117  		if err = yaml.Unmarshal(b, dstCRD); err != nil {
   118  			return nil, err
   119  		}
   120  		val := dstCRD.Spec.Validation.DeepCopy()
   121  
   122  		// If the crd exists at i.Path, append the validation spec to its crd spec.
   123  		if _, err := os.Stat(i.Path); err == nil {
   124  			cb, err := ioutil.ReadFile(i.Path)
   125  			if err != nil {
   126  				return nil, err
   127  			}
   128  			if len(cb) > 0 {
   129  				dstCRD = &apiextv1beta1.CustomResourceDefinition{}
   130  				if err = yaml.Unmarshal(cb, dstCRD); err != nil {
   131  					return nil, err
   132  				}
   133  				dstCRD.Spec.Validation = val
   134  			}
   135  		}
   136  		// controller-tools does not set ListKind or Singular names.
   137  		dstCRD.Spec.Names = getCRDNamesForResource(s.Resource)
   138  		// Remove controller-tools default label.
   139  		delete(dstCRD.Labels, "controller-tools.k8s.io")
   140  	}
   141  	addCRDSubresource(dstCRD)
   142  	addCRDVersions(dstCRD)
   143  	return getCRDBytes(dstCRD)
   144  }
   145  
   146  func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition {
   147  	return &apiextv1beta1.CustomResourceDefinition{
   148  		TypeMeta: metav1.TypeMeta{
   149  			APIVersion: "apiextensions.k8s.io/v1beta1",
   150  			Kind:       "CustomResourceDefinition",
   151  		},
   152  		ObjectMeta: metav1.ObjectMeta{
   153  			Name: r.Resource + "." + r.FullGroup,
   154  		},
   155  		Spec: apiextv1beta1.CustomResourceDefinitionSpec{
   156  			Group:   r.FullGroup,
   157  			Names:   getCRDNamesForResource(r),
   158  			Scope:   apiextv1beta1.NamespaceScoped,
   159  			Version: r.Version,
   160  			Subresources: &apiextv1beta1.CustomResourceSubresources{
   161  				Status: &apiextv1beta1.CustomResourceSubresourceStatus{},
   162  			},
   163  		},
   164  	}
   165  }
   166  
   167  func getCRDNamesForResource(r *Resource) apiextv1beta1.CustomResourceDefinitionNames {
   168  	return apiextv1beta1.CustomResourceDefinitionNames{
   169  		Kind:     r.Kind,
   170  		ListKind: r.Kind + "List",
   171  		Plural:   r.Resource,
   172  		Singular: r.LowerKind,
   173  	}
   174  }
   175  
   176  func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) {
   177  	if crd.Spec.Subresources == nil {
   178  		crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{}
   179  	}
   180  	if crd.Spec.Subresources.Status == nil {
   181  		crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{}
   182  	}
   183  }
   184  
   185  func addCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) {
   186  	// crd.Version is deprecated, use crd.Versions instead.
   187  	var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion
   188  	if crd.Spec.Version != "" {
   189  		var verExists, hasStorageVer bool
   190  		for _, ver := range crd.Spec.Versions {
   191  			if crd.Spec.Version == ver.Name {
   192  				verExists = true
   193  			}
   194  			// There must be exactly one version flagged as a storage version.
   195  			if ver.Storage {
   196  				hasStorageVer = true
   197  			}
   198  		}
   199  		if !verExists {
   200  			crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{
   201  				{Name: crd.Spec.Version, Served: true, Storage: !hasStorageVer},
   202  			}
   203  		}
   204  	} else {
   205  		crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{
   206  			{Name: "v1alpha1", Served: true, Storage: true},
   207  		}
   208  	}
   209  
   210  	if len(crd.Spec.Versions) > 0 {
   211  		// crd.Version should always be the first element in crd.Versions.
   212  		crd.Spec.Versions = append(crdVersions, crd.Spec.Versions...)
   213  	} else {
   214  		crd.Spec.Versions = crdVersions
   215  	}
   216  }
   217  
   218  func getCRDBytes(crd *apiextv1beta1.CustomResourceDefinition) ([]byte, error) {
   219  	// Remove the "status" field from yaml data, which causes a
   220  	// resource creation error.
   221  	crdMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(crd)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	delete(crdMap, "status")
   226  	return yaml.Marshal(&crdMap)
   227  }