github.com/giantswarm/apiextensions/v6@v6.6.0/pkg/crd/renderer.go (about)

     1  package crd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/giantswarm/microerror"
    15  	"github.com/google/go-github/v39/github"
    16  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    17  	"k8s.io/apimachinery/pkg/runtime/schema"
    18  )
    19  
    20  var (
    21  	// Kubernetes API group, version and kind for v1 CRDs
    22  	crdV1GVK = schema.GroupVersionKind{
    23  		Group:   "apiextensions.k8s.io",
    24  		Version: "v1",
    25  		Kind:    "CustomResourceDefinition",
    26  	}
    27  )
    28  
    29  func (r Renderer) patchCRDs(provider string, crds []v1.CustomResourceDefinition) ([]v1.CustomResourceDefinition, error) {
    30  	patchedCRDs := make([]v1.CustomResourceDefinition, 0, len(crds))
    31  	for _, crd := range crds {
    32  		patched, err := patchCRD(provider, r.Patches, crd)
    33  		if err != nil {
    34  			return nil, microerror.Mask(err)
    35  		}
    36  		patchedCRDs = append(patchedCRDs, patched)
    37  	}
    38  	return patchedCRDs, nil
    39  }
    40  
    41  // Render creates helm chart templates for the given provider by downloading upstream CRDs, merging them with local
    42  // CRDs, patching them, and writing them to the corresponding provider helm template directory.
    43  func (r Renderer) Render(ctx context.Context, provider string) error {
    44  	outputDirectory := filepath.Join(helmChartDirectory(r.OutputDirectory, provider), "templates")
    45  
    46  	{
    47  		localCRDs, err := r.getLocalCRDs(provider)
    48  		if err != nil {
    49  			return microerror.Mask(err)
    50  		}
    51  
    52  		remoteCRDs, err := r.getRemoteCRDs(ctx, provider)
    53  		if err != nil {
    54  			return microerror.Mask(err)
    55  		}
    56  
    57  		internalCRDs, err := r.patchCRDs(provider, append(localCRDs, remoteCRDs...))
    58  		if err != nil {
    59  			return microerror.Mask(err)
    60  		}
    61  
    62  		err = writeCRDsToDirectory(outputDirectory, internalCRDs)
    63  		if err != nil {
    64  			return microerror.Mask(err)
    65  		}
    66  	}
    67  
    68  	{
    69  		upstreamCRDs, err := r.getUpstreamCRDs(ctx, provider)
    70  		if err != nil {
    71  			return microerror.Mask(err)
    72  		}
    73  
    74  		upstreamCRDs, err = r.patchCRDs(provider, upstreamCRDs)
    75  		if err != nil {
    76  			return microerror.Mask(err)
    77  		}
    78  
    79  		err = writeCRDsToDirectory(outputDirectory, upstreamCRDs)
    80  		if err != nil {
    81  			return microerror.Mask(err)
    82  		}
    83  	}
    84  
    85  	return nil
    86  }
    87  
    88  // downloadReleaseAssetCRDs returns a slice of CRDs by downloading the given GitHub release asset, parsing it as YAML,
    89  // and filtering for only CRD objects.
    90  func (r Renderer) downloadReleaseAssetCRDs(ctx context.Context, asset ReleaseAssetFileDefinition) ([]v1.CustomResourceDefinition, error) {
    91  	release, _, err := r.GithubClient.Repositories.GetReleaseByTag(ctx, asset.Owner, asset.Repo, asset.Version)
    92  	if err != nil {
    93  		return nil, microerror.Mask(err)
    94  	}
    95  
    96  	var targetAssets []*github.ReleaseAsset
    97  	for _, releaseAsset := range release.Assets {
    98  		for _, file := range asset.Files {
    99  			if releaseAsset.GetName() == file {
   100  				targetAssets = append(targetAssets, releaseAsset)
   101  			}
   102  		}
   103  	}
   104  	if targetAssets == nil {
   105  		return nil, notFoundError
   106  	}
   107  
   108  	var allCrds []v1.CustomResourceDefinition
   109  	for _, targetAsset := range targetAssets {
   110  		contentReader, _, err := r.GithubClient.Repositories.DownloadReleaseAsset(ctx, asset.Owner, asset.Repo, targetAsset.GetID(), http.DefaultClient)
   111  		if err != nil {
   112  			return nil, microerror.Mask(err)
   113  		}
   114  
   115  		crds, err := decodeCRDs(contentReader)
   116  		if err != nil {
   117  			return nil, microerror.Mask(err)
   118  		}
   119  
   120  		allCrds = append(allCrds, crds...)
   121  	}
   122  
   123  	return allCrds, nil
   124  }
   125  
   126  // getUpstreamCRDs returns all upstream CRDs for a provider based on the Renderer's upstream asset configuration.
   127  func (r Renderer) getUpstreamCRDs(ctx context.Context, provider string) ([]v1.CustomResourceDefinition, error) {
   128  	var crds []v1.CustomResourceDefinition
   129  	for _, releaseAsset := range r.UpstreamAssets {
   130  		if releaseAsset.Provider != provider {
   131  			continue
   132  		}
   133  
   134  		releaseAssetCRDs, err := r.downloadReleaseAssetCRDs(ctx, releaseAsset)
   135  		if err != nil {
   136  			return nil, microerror.Mask(err)
   137  		}
   138  
   139  		crds = append(crds, releaseAssetCRDs...)
   140  	}
   141  
   142  	return crds, nil
   143  }
   144  
   145  // getLocalCRDs reads the configured local directory and returns a slice of CRDs that have the given category.
   146  func (r Renderer) getLocalCRDs(category string) ([]v1.CustomResourceDefinition, error) {
   147  	var crds []v1.CustomResourceDefinition
   148  	err := filepath.WalkDir(r.LocalCRDDirectory, func(path string, entry os.DirEntry, walkErr error) error {
   149  		if walkErr != nil {
   150  			return microerror.Mask(walkErr)
   151  		}
   152  		if entry.IsDir() {
   153  			return nil
   154  		}
   155  
   156  		file, err := os.Open(path)
   157  		if err != nil {
   158  			return microerror.Mask(err)
   159  		}
   160  
   161  		fileCRDs, err := decodeCRDs(file)
   162  		if err != nil {
   163  			return microerror.Mask(err)
   164  		}
   165  
   166  		for _, crd := range fileCRDs {
   167  			if contains(crd.Spec.Names.Categories, category) {
   168  				crds = append(crds, crd)
   169  			}
   170  		}
   171  
   172  		return nil
   173  	})
   174  	if err != nil {
   175  		return nil, microerror.Mask(err)
   176  	}
   177  
   178  	return crds, nil
   179  }
   180  
   181  // getRemoteCRDs returns all remote CRDs for a provider based on the Renderer's remote repository configuration.
   182  func (r Renderer) getRemoteCRDs(ctx context.Context, provider string) ([]v1.CustomResourceDefinition, error) {
   183  	var crds []v1.CustomResourceDefinition
   184  	for _, releaseAsset := range r.RemoteRepositories {
   185  		if releaseAsset.Provider != provider {
   186  			continue
   187  		}
   188  
   189  		remoteCRDs, err := r.downloadRepositoryCRDs(ctx, releaseAsset)
   190  		if err != nil {
   191  			return nil, microerror.Mask(err)
   192  		}
   193  
   194  		crds = append(crds, remoteCRDs...)
   195  	}
   196  
   197  	return crds, nil
   198  }
   199  
   200  // downloadRepositoryCRDs returns a slice of CRDs by downloading the given GitHub repository tree, listing files in the
   201  // given path, parsing them as YAML, and filtering for only CRD objects.
   202  func (r Renderer) downloadRepositoryCRDs(ctx context.Context, repo RemoteRepositoryDefinition) ([]v1.CustomResourceDefinition, error) {
   203  	refString := fmt.Sprintf("tags/%s", repo.Reference)
   204  	ref, response, err := r.GithubClient.Git.GetRef(ctx, repo.Owner, repo.Name, refString)
   205  	if err != nil && response.StatusCode == 404 {
   206  		refString = fmt.Sprintf("heads/%s", repo.Reference)
   207  		ref, _, err = r.GithubClient.Git.GetRef(ctx, repo.Owner, repo.Name, refString)
   208  	}
   209  	if err != nil {
   210  		return nil, microerror.Mask(err)
   211  	}
   212  
   213  	commit, _, err := r.GithubClient.Git.GetCommit(ctx, repo.Owner, repo.Name, ref.Object.GetSHA())
   214  	if err != nil {
   215  		return nil, microerror.Mask(err)
   216  	}
   217  
   218  	tree, _, err := r.GithubClient.Git.GetTree(ctx, repo.Owner, repo.Name, commit.Tree.GetSHA(), true)
   219  	if err != nil {
   220  		return nil, microerror.Mask(err)
   221  	}
   222  
   223  	var targetEntries []*github.TreeEntry
   224  	for _, entry := range tree.Entries {
   225  		if entry.GetType() == "blob" && strings.HasPrefix(entry.GetPath(), repo.Path) {
   226  			targetEntries = append(targetEntries, entry)
   227  		}
   228  	}
   229  	if targetEntries == nil {
   230  		return nil, notFoundError
   231  	}
   232  
   233  	var allCrds []v1.CustomResourceDefinition
   234  	for _, entry := range targetEntries {
   235  		blob, _, err := r.GithubClient.Git.GetBlob(ctx, repo.Owner, repo.Name, entry.GetSHA())
   236  		if err != nil {
   237  			return nil, microerror.Mask(err)
   238  		}
   239  
   240  		content, err := base64.StdEncoding.DecodeString(blob.GetContent())
   241  		if err != nil {
   242  			return nil, microerror.Mask(err)
   243  		}
   244  
   245  		contentReader := io.NopCloser(bytes.NewReader(content))
   246  
   247  		crds, err := decodeCRDs(contentReader)
   248  		if err != nil {
   249  			return nil, microerror.Mask(err)
   250  		}
   251  
   252  		allCrds = append(allCrds, crds...)
   253  	}
   254  
   255  	return allCrds, nil
   256  }