github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/render/azureaks/render.go (about)

     1  package azureaks
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"path"
     8  	"regexp"
     9  	"strings"
    10  	"text/template"
    11  
    12  	"github.com/Masterminds/sprig/v3"
    13  	"github.com/go-kit/kit/log"
    14  	multierror "github.com/hashicorp/go-multierror"
    15  	"github.com/pkg/errors"
    16  	"github.com/replicatedhq/libyaml"
    17  	"github.com/replicatedhq/ship/pkg/api"
    18  	"github.com/replicatedhq/ship/pkg/lifecycle/render/inline"
    19  	"github.com/replicatedhq/ship/pkg/lifecycle/render/root"
    20  	"github.com/replicatedhq/ship/pkg/templates"
    21  	"github.com/spf13/afero"
    22  )
    23  
    24  // Renderer is something that can render a terraform asset (that produces an AKS cluster) as part of a planner.Plan
    25  type Renderer interface {
    26  	Execute(
    27  		rootFs root.Fs,
    28  		asset api.AKSAsset,
    29  		meta api.ReleaseMetadata,
    30  		templateContext map[string]interface{},
    31  		configGroups []libyaml.ConfigGroup,
    32  	) func(ctx context.Context) error
    33  }
    34  
    35  // a LocalRenderer renders a terraform asset by writing generated terraform source code
    36  type LocalRenderer struct {
    37  	Logger         log.Logger
    38  	BuilderBuilder *templates.BuilderBuilder
    39  	Inline         inline.Renderer
    40  	Fs             afero.Afero
    41  }
    42  
    43  var _ Renderer = &LocalRenderer{}
    44  
    45  func NewRenderer(
    46  	logger log.Logger,
    47  	bb *templates.BuilderBuilder,
    48  	inline inline.Renderer,
    49  	fs afero.Afero,
    50  ) Renderer {
    51  	return &LocalRenderer{
    52  		Logger:         logger,
    53  		BuilderBuilder: bb,
    54  		Inline:         inline,
    55  		Fs:             fs,
    56  	}
    57  }
    58  
    59  func (r *LocalRenderer) Execute(
    60  	rootFs root.Fs,
    61  	asset api.AKSAsset,
    62  	meta api.ReleaseMetadata,
    63  	templateContext map[string]interface{},
    64  	configGroups []libyaml.ConfigGroup,
    65  ) func(ctx context.Context) error {
    66  	return func(ctx context.Context) error {
    67  		builder, err := r.BuilderBuilder.FullBuilder(meta, configGroups, templateContext)
    68  		if err != nil {
    69  			return errors.Wrap(err, "init builder")
    70  		}
    71  
    72  		asset, err = buildAsset(asset, builder)
    73  		if err != nil {
    74  			return errors.Wrap(err, "build asset")
    75  		}
    76  
    77  		assetsPath := "azure_aks.tf"
    78  		if asset.Dest != "" {
    79  			assetsPath = asset.Dest
    80  		}
    81  		kubeConfigPath := path.Join(path.Dir(assetsPath), "kubeconfig_"+asset.ClusterName)
    82  
    83  		contents, err := renderTerraformContents(asset, kubeConfigPath)
    84  		if err != nil {
    85  			return errors.Wrap(err, "render tf config")
    86  		}
    87  
    88  		templates.AddAzureAKSPath(asset.ClusterName, kubeConfigPath)
    89  
    90  		// write the inline spec
    91  		err = r.Inline.Execute(
    92  			rootFs,
    93  			api.InlineAsset{
    94  				Contents: contents,
    95  				AssetShared: api.AssetShared{
    96  					Dest: assetsPath,
    97  					Mode: asset.Mode,
    98  				},
    99  			},
   100  			meta,
   101  			templateContext,
   102  			configGroups,
   103  		)(ctx)
   104  
   105  		if err != nil {
   106  			return errors.Wrap(err, "write tf config")
   107  		}
   108  		return nil
   109  	}
   110  }
   111  
   112  // build asset values used outside of the terraform inline asset
   113  func buildAsset(asset api.AKSAsset, builder *templates.Builder) (api.AKSAsset, error) {
   114  	var err error
   115  	var multiErr *multierror.Error
   116  
   117  	asset.ClusterName, err = builder.String(asset.ClusterName)
   118  	multiErr = multierror.Append(multiErr, errors.Wrap(err, "build cluster_name"))
   119  
   120  	asset.Dest, err = builder.String(asset.Dest)
   121  	multiErr = multierror.Append(multiErr, errors.Wrap(err, "build dest"))
   122  
   123  	return asset, multiErr.ErrorOrNil()
   124  }
   125  
   126  func renderTerraformContents(asset api.AKSAsset, kubeConfigPath string) (string, error) {
   127  	t, err := template.New("aksTemplate").
   128  		Funcs(sprig.TxtFuncMap()).
   129  		Parse(clusterTempl)
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  	return executeTemplate(t, asset, kubeConfigPath)
   134  }
   135  
   136  func executeTemplate(t *template.Template, asset api.AKSAsset, kubeConfigPath string) (string, error) {
   137  	var data = struct {
   138  		api.AKSAsset
   139  		KubeConfigPath  string
   140  		SafeClusterName string
   141  	}{
   142  		asset,
   143  		kubeConfigPath,
   144  		safeClusterName(asset.ClusterName),
   145  	}
   146  	var tpl bytes.Buffer
   147  	if err := t.Execute(&tpl, data); err != nil {
   148  		return "", err
   149  	}
   150  
   151  	return tpl.String(), nil
   152  }
   153  
   154  // Create a string from the clusterName safe for use as the agent pool name and
   155  // the dns_prefix.
   156  // "Agent Pool names must start with a lowercase letter, have max length of 12, and only have characters a-z0-9"
   157  var unsafeClusterNameChars = regexp.MustCompile(`[^a-z0-9]`)
   158  var startsWithLower = regexp.MustCompile(`^[a-z]`)
   159  
   160  func safeClusterName(clusterName string) string {
   161  	s := strings.ToLower(clusterName)
   162  	s = unsafeClusterNameChars.ReplaceAllString(s, "")
   163  	for !startsWithLower.MatchString(s) && len(s) > 0 {
   164  		s = s[1:]
   165  	}
   166  	if len(s) > 12 {
   167  		return s[0:12]
   168  	}
   169  	if len(s) == 0 {
   170  		return safeClusterName(base64.StdEncoding.EncodeToString([]byte("cluster" + clusterName)))
   171  	}
   172  	return s
   173  }