|
1 | 1 | package dynamicparameters
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | +"archive/tar" |
| 5 | +"bytes" |
| 6 | +"context" |
4 | 7 | _"embed"
|
5 | 8 | "encoding/json"
|
| 9 | +"io" |
| 10 | +"path/filepath" |
| 11 | +"slices" |
6 | 12 | "strings"
|
7 | 13 | "text/template"
|
8 | 14 |
|
| 15 | +"github.com/google/uuid" |
| 16 | +"golang.org/x/xerrors" |
| 17 | + |
| 18 | +"cdr.dev/slog" |
| 19 | +"github.com/coder/coder/v2/codersdk" |
9 | 20 | "github.com/coder/coder/v2/cryptorand"
|
10 | 21 | )
|
11 | 22 |
|
@@ -72,3 +83,187 @@ func GetModuleFiles() map[string][]byte {
|
72 | 83 | ".terraform/modules/modules.json":modulesJSONBytes,
|
73 | 84 | }
|
74 | 85 | }
|
| 86 | + |
| 87 | +funccreateTarFromFiles(filesmap[string][]byte) ([]byte,error) { |
| 88 | +buf:=new(bytes.Buffer) |
| 89 | +writer:=tar.NewWriter(buf) |
| 90 | +dirs:= []string{} |
| 91 | +forname,content:=rangefiles { |
| 92 | +// We need to add directories before any files that use them. But, we only need to do this |
| 93 | +// once. |
| 94 | +dir:=filepath.Dir(name) |
| 95 | +ifdir!="."&&!slices.Contains(dirs,dir) { |
| 96 | +dirs=append(dirs,dir) |
| 97 | +err:=writer.WriteHeader(&tar.Header{ |
| 98 | +Name:dir, |
| 99 | +Mode:0o755, |
| 100 | +Typeflag:tar.TypeDir, |
| 101 | +}) |
| 102 | +iferr!=nil { |
| 103 | +returnnil,err |
| 104 | +} |
| 105 | +} |
| 106 | + |
| 107 | +err:=writer.WriteHeader(&tar.Header{ |
| 108 | +Name:name, |
| 109 | +Size:int64(len(content)), |
| 110 | +Mode:0o644, |
| 111 | +}) |
| 112 | +iferr!=nil { |
| 113 | +returnnil,err |
| 114 | +} |
| 115 | + |
| 116 | +_,err=writer.Write(content) |
| 117 | +iferr!=nil { |
| 118 | +returnnil,err |
| 119 | +} |
| 120 | +} |
| 121 | +// `writer.Close()` function flushes the writer buffer, and adds extra padding to create a legal tarball. |
| 122 | +err:=writer.Close() |
| 123 | +iferr!=nil { |
| 124 | +returnnil,err |
| 125 | +} |
| 126 | +returnbuf.Bytes(),nil |
| 127 | +} |
| 128 | + |
| 129 | +funcTemplateTarData() ([]byte,error) { |
| 130 | +mainTF,err:=TemplateContent() |
| 131 | +iferr!=nil { |
| 132 | +returnnil,xerrors.Errorf("failed to generate main.tf: %w",err) |
| 133 | +} |
| 134 | +moduleFiles:=GetModuleFiles() |
| 135 | + |
| 136 | +files:=map[string][]byte{ |
| 137 | +"main.tf": []byte(mainTF), |
| 138 | +} |
| 139 | +fork,v:=rangemoduleFiles { |
| 140 | +files[k]=v |
| 141 | +} |
| 142 | +tarData,err:=createTarFromFiles(files) |
| 143 | +iferr!=nil { |
| 144 | +returnnil,xerrors.Errorf("failed to create tarball: %w",err) |
| 145 | +} |
| 146 | + |
| 147 | +returntarData,nil |
| 148 | +} |
| 149 | + |
| 150 | +typePartitionstruct { |
| 151 | +TemplateVersion codersdk.TemplateVersion |
| 152 | +ConcurrentEvaluationsint |
| 153 | +} |
| 154 | + |
| 155 | +typeSDKForDynamicParametersSetupinterface { |
| 156 | +TemplateByName(ctx context.Context,orgID uuid.UUID,templateNamestring) (codersdk.Template,error) |
| 157 | +CreateTemplate(ctx context.Context,orgID uuid.UUID,createReq codersdk.CreateTemplateRequest) (codersdk.Template,error) |
| 158 | +CreateTemplateVersion(ctx context.Context,orgID uuid.UUID,createReq codersdk.CreateTemplateVersionRequest) (codersdk.TemplateVersion,error) |
| 159 | +Upload(ctx context.Context,contentTypestring,reader io.Reader) (codersdk.UploadResponse,error) |
| 160 | +} |
| 161 | + |
| 162 | +funcSetupPartitions( |
| 163 | +ctx context.Context,clientSDKForDynamicParametersSetup, |
| 164 | +orgID uuid.UUID,templateNamestring,numEvalsint64, |
| 165 | +logger slog.Logger, |
| 166 | +) ([]Partition,error) { |
| 167 | +var ( |
| 168 | +errerror |
| 169 | +coderError*codersdk.Error |
| 170 | +templ codersdk.Template |
| 171 | +tempVersion codersdk.TemplateVersion |
| 172 | +) |
| 173 | +templ,err=client.TemplateByName(ctx,orgID,templateName) |
| 174 | +ifxerrors.As(err,&coderError)&&coderError.StatusCode()==404 { |
| 175 | +tempVersion,err=createTemplateVersion(ctx,client,orgID,uuid.Nil) |
| 176 | +iferr!=nil { |
| 177 | +returnnil,xerrors.Errorf("failed to create template version: %w",err) |
| 178 | +} |
| 179 | +logger.Info(ctx,"created template version",slog.F("version_id",tempVersion.ID)) |
| 180 | +createReq:= codersdk.CreateTemplateRequest{ |
| 181 | +Name:templateName, |
| 182 | +DisplayName:"Scaletest Dynamic Parameters", |
| 183 | +Description:"`coder exp scaletest dynamic parameters test` template", |
| 184 | +VersionID:tempVersion.ID, |
| 185 | +} |
| 186 | +templ,err=client.CreateTemplate(ctx,orgID,createReq) |
| 187 | +iferr!=nil { |
| 188 | +returnnil,xerrors.Errorf("failed to create template: %w",err) |
| 189 | +} |
| 190 | +logger.Info(ctx,"created template",slog.F("template_id",templ.ID),slog.F("name",templateName)) |
| 191 | +}elseiferr!=nil { |
| 192 | +returnnil,xerrors.Errorf("failed to get template: %w",err) |
| 193 | +} |
| 194 | + |
| 195 | +// Partition the number into a list decreasing by half each time |
| 196 | +evalParts:=partitionEvaluations(int(numEvals)) |
| 197 | +logger.Info(ctx,"partitioned evaluations",slog.F("num_evals",numEvals),slog.F("eval_parts",evalParts)) |
| 198 | + |
| 199 | +// If tempVersion is not empty (i.e. we created it above), use it as the first version. |
| 200 | +partitions:=make([]Partition,0,len(evalParts)) |
| 201 | +iftempVersion.ID!=uuid.Nil { |
| 202 | +partitions=append(partitions,Partition{ |
| 203 | +TemplateVersion:tempVersion, |
| 204 | +ConcurrentEvaluations:evalParts[0], |
| 205 | +}) |
| 206 | +evalParts=evalParts[1:] |
| 207 | +} |
| 208 | + |
| 209 | +for_,num:=rangeevalParts { |
| 210 | +version,err:=createTemplateVersion(ctx,client,orgID,templ.ID) |
| 211 | +iferr!=nil { |
| 212 | +returnnil,xerrors.Errorf("failed to create template version: %w",err) |
| 213 | +} |
| 214 | +partitions=append(partitions,Partition{ |
| 215 | +TemplateVersion:version, |
| 216 | +ConcurrentEvaluations:num, |
| 217 | +}) |
| 218 | +logger.Info(ctx,"created template version",slog.F("version_id",version.ID)) |
| 219 | +} |
| 220 | +returnpartitions,nil |
| 221 | +} |
| 222 | + |
| 223 | +// createTemplateVersion generates the template tarball, uploads it, and creates a template version. Returns the version and any error. |
| 224 | +// If templateID is not uuid.Nil, it will be used to update an existing template instead of creating a new one. |
| 225 | +funccreateTemplateVersion(ctx context.Context,clientSDKForDynamicParametersSetup,orgID uuid.UUID,templateID uuid.UUID) (codersdk.TemplateVersion,error) { |
| 226 | +tarData,err:=TemplateTarData() |
| 227 | +iferr!=nil { |
| 228 | +return codersdk.TemplateVersion{},xerrors.Errorf("failed to create template tarball: %w",err) |
| 229 | +} |
| 230 | + |
| 231 | +// Upload tarball |
| 232 | +uploadResp,err:=client.Upload(ctx,codersdk.ContentTypeTar,bytes.NewReader(tarData)) |
| 233 | +iferr!=nil { |
| 234 | +return codersdk.TemplateVersion{},xerrors.Errorf("failed to upload template tar: %w",err) |
| 235 | +} |
| 236 | + |
| 237 | +// Create template version |
| 238 | +versionReq:= codersdk.CreateTemplateVersionRequest{ |
| 239 | +TemplateID:templateID, |
| 240 | +FileID:uploadResp.ID, |
| 241 | +Message:"Initial version for scaletest dynamic parameters", |
| 242 | +StorageMethod:codersdk.ProvisionerStorageMethodFile, |
| 243 | +Provisioner:codersdk.ProvisionerTypeTerraform, |
| 244 | +} |
| 245 | +version,err:=client.CreateTemplateVersion(ctx,orgID,versionReq) |
| 246 | +iferr!=nil { |
| 247 | +return codersdk.TemplateVersion{},xerrors.Errorf("failed to create template version: %w",err) |
| 248 | +} |
| 249 | +returnversion,nil |
| 250 | +} |
| 251 | + |
| 252 | +// partitionEvaluations partitions a number into a list decreasing by half each time. |
| 253 | +funcpartitionEvaluations(totalint) []int { |
| 254 | +varparts []int |
| 255 | +remaining:=total |
| 256 | +forremaining>0 { |
| 257 | +next:=remaining/2 |
| 258 | +// round up |
| 259 | +ifnext*2!=remaining { |
| 260 | +next++ |
| 261 | +} |
| 262 | +ifnext>remaining { |
| 263 | +next=remaining |
| 264 | +} |
| 265 | +parts=append(parts,next) |
| 266 | +remaining-=next |
| 267 | +} |
| 268 | +returnparts |
| 269 | +} |