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 }