mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: Expose managed variables via API (#6134)
* WIP * hcl * useManagedVariables * fix * Fix * Fix * fix * go:build * Fix * fix: bool flag * Insert template variables * API * fix * Expose via API * More wiring * CLI for testing purposes * WIP * Delete FIXME * planVars * WIP * WIP * UserVariableValues * no dry run * Dry run * Done FIXME * Fix * Fix: CLI * Fix: migration * API tests * Test info * Tests * More tests * fix: lint * Fix: authz * Address PR comments * Fix * fix * fix
This commit is contained in:
@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -145,6 +146,10 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("get template version: %s", err))
|
||||
}
|
||||
templateVariables, err := server.Database.GetTemplateVersionVariables(ctx, templateVersion.ID)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
return nil, failJob(fmt.Sprintf("get template version variables: %s", err))
|
||||
}
|
||||
template, err := server.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("get template: %s", err))
|
||||
@ -196,6 +201,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
|
||||
State: workspaceBuild.ProvisionerState,
|
||||
ParameterValues: protoParameters,
|
||||
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
|
||||
VariableValues: asVariableValues(templateVariables),
|
||||
Metadata: &sdkproto.Provision_Metadata{
|
||||
CoderUrl: server.AccessURL.String(),
|
||||
WorkspaceTransition: transition,
|
||||
@ -218,6 +224,10 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("get template version: %s", err))
|
||||
}
|
||||
templateVariables, err := server.Database.GetTemplateVersionVariables(ctx, templateVersion.ID)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
return nil, failJob(fmt.Sprintf("get template version variables: %s", err))
|
||||
}
|
||||
|
||||
// Compute parameters for the dry-run to consume.
|
||||
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
|
||||
@ -240,6 +250,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
|
||||
TemplateDryRun: &proto.AcquiredJob_TemplateDryRun{
|
||||
ParameterValues: protoParameters,
|
||||
RichParameterValues: convertRichParameterValues(input.RichParameterValues),
|
||||
VariableValues: asVariableValues(templateVariables),
|
||||
Metadata: &sdkproto.Provision_Metadata{
|
||||
CoderUrl: server.AccessURL.String(),
|
||||
WorkspaceName: input.WorkspaceName,
|
||||
@ -247,8 +258,15 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
|
||||
},
|
||||
}
|
||||
case database.ProvisionerJobTypeTemplateVersionImport:
|
||||
var input TemplateVersionImportJob
|
||||
err = json.Unmarshal(job.Input, &input)
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("unmarshal job input %q: %s", job.Input, err))
|
||||
}
|
||||
|
||||
protoJob.Type = &proto.AcquiredJob_TemplateImport_{
|
||||
TemplateImport: &proto.AcquiredJob_TemplateImport{
|
||||
UserVariableValues: convertVariableValues(input.UserVariableValues),
|
||||
Metadata: &sdkproto.Provision_Metadata{
|
||||
CoderUrl: server.AccessURL.String(),
|
||||
},
|
||||
@ -387,6 +405,61 @@ func (server *Server) UpdateJob(ctx context.Context, request *proto.UpdateJobReq
|
||||
}
|
||||
}
|
||||
|
||||
if len(request.TemplateVariables) > 0 {
|
||||
templateVersion, err := server.Database.GetTemplateVersionByJobID(ctx, job.ID)
|
||||
if err != nil {
|
||||
server.Logger.Error(ctx, "failed to get the template version", slog.F("job_id", parsedID), slog.Error(err))
|
||||
return nil, xerrors.Errorf("get template version by job id: %w", err)
|
||||
}
|
||||
|
||||
var variableValues []*sdkproto.VariableValue
|
||||
var variablesWithMissingValues []string
|
||||
for _, templateVariable := range request.TemplateVariables {
|
||||
server.Logger.Debug(ctx, "insert template variable", slog.F("template_version_id", templateVersion.ID), slog.F("template_variable", redactTemplateVariable(templateVariable)))
|
||||
|
||||
var value = templateVariable.DefaultValue
|
||||
for _, v := range request.UserVariableValues {
|
||||
if v.Name == templateVariable.Name {
|
||||
value = v.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if templateVariable.Required && value == "" {
|
||||
variablesWithMissingValues = append(variablesWithMissingValues, templateVariable.Name)
|
||||
}
|
||||
|
||||
variableValues = append(variableValues, &sdkproto.VariableValue{
|
||||
Name: templateVariable.Name,
|
||||
Value: value,
|
||||
Sensitive: templateVariable.Sensitive,
|
||||
})
|
||||
|
||||
_, err = server.Database.InsertTemplateVersionVariable(ctx, database.InsertTemplateVersionVariableParams{
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
Name: templateVariable.Name,
|
||||
Description: templateVariable.Description,
|
||||
Type: templateVariable.Type,
|
||||
DefaultValue: templateVariable.DefaultValue,
|
||||
Required: templateVariable.Required,
|
||||
Sensitive: templateVariable.Sensitive,
|
||||
Value: value,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert parameter schema: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(variablesWithMissingValues) > 0 {
|
||||
return nil, xerrors.Errorf("required template variables need values: %s", strings.Join(variablesWithMissingValues, ", "))
|
||||
}
|
||||
|
||||
return &proto.UpdateJobResponse{
|
||||
Canceled: job.CanceledAt.Valid,
|
||||
VariableValues: variableValues,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(request.ParameterSchemas) > 0 {
|
||||
for index, protoParameter := range request.ParameterSchemas {
|
||||
validationTypeSystem, err := convertValidationTypeSystem(protoParameter.ValidationTypeSystem)
|
||||
@ -1145,6 +1218,17 @@ func convertRichParameterValues(workspaceBuildParameters []database.WorkspaceBui
|
||||
return protoParameters
|
||||
}
|
||||
|
||||
func convertVariableValues(variableValues []codersdk.VariableValue) []*sdkproto.VariableValue {
|
||||
protoVariableValues := make([]*sdkproto.VariableValue, len(variableValues))
|
||||
for i, variableValue := range variableValues {
|
||||
protoVariableValues[i] = &sdkproto.VariableValue{
|
||||
Name: variableValue.Name,
|
||||
Value: variableValue.Value,
|
||||
}
|
||||
}
|
||||
return protoVariableValues
|
||||
}
|
||||
|
||||
func convertComputedParameterValues(parameters []parameter.ComputedValue) ([]*sdkproto.ParameterValue, error) {
|
||||
protoParameters := make([]*sdkproto.ParameterValue, len(parameters))
|
||||
for i, computedParameter := range parameters {
|
||||
@ -1203,7 +1287,8 @@ func auditActionFromTransition(transition database.WorkspaceTransition) database
|
||||
}
|
||||
|
||||
type TemplateVersionImportJob struct {
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id"`
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id"`
|
||||
UserVariableValues []codersdk.VariableValue `json:"user_variable_values"`
|
||||
}
|
||||
|
||||
// WorkspaceProvisionJob is the payload for the "workspace_provision" job type.
|
||||
@ -1232,3 +1317,40 @@ type ProvisionerJobLogsNotifyMessage struct {
|
||||
func ProvisionerJobLogsNotifyChannel(jobID uuid.UUID) string {
|
||||
return fmt.Sprintf("provisioner-log-logs:%s", jobID)
|
||||
}
|
||||
|
||||
func asVariableValues(templateVariables []database.TemplateVersionVariable) []*sdkproto.VariableValue {
|
||||
var apiVariableValues []*sdkproto.VariableValue
|
||||
for _, v := range templateVariables {
|
||||
var value = v.Value
|
||||
if value == "" && v.DefaultValue != "" {
|
||||
value = v.DefaultValue
|
||||
}
|
||||
|
||||
if value != "" || v.Required {
|
||||
apiVariableValues = append(apiVariableValues, &sdkproto.VariableValue{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
Sensitive: v.Sensitive,
|
||||
})
|
||||
}
|
||||
}
|
||||
return apiVariableValues
|
||||
}
|
||||
|
||||
func redactTemplateVariable(templateVariable *sdkproto.TemplateVariable) *sdkproto.TemplateVariable {
|
||||
if templateVariable == nil {
|
||||
return nil
|
||||
}
|
||||
maybeRedacted := &sdkproto.TemplateVariable{
|
||||
Name: templateVariable.Name,
|
||||
Description: templateVariable.Description,
|
||||
Type: templateVariable.Type,
|
||||
DefaultValue: templateVariable.DefaultValue,
|
||||
Required: templateVariable.Required,
|
||||
Sensitive: templateVariable.Sensitive,
|
||||
}
|
||||
if maybeRedacted.Sensitive {
|
||||
maybeRedacted.DefaultValue = "*redacted*"
|
||||
}
|
||||
return maybeRedacted
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func mockAuditor() *atomic.Pointer[audit.Auditor] {
|
||||
@ -113,8 +114,26 @@ func TestAcquireJob(t *testing.T) {
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{
|
||||
TemplateVersionID: version.ID,
|
||||
UserVariableValues: []codersdk.VariableValue{
|
||||
{Name: "second", Value: "bah"},
|
||||
},
|
||||
})),
|
||||
})
|
||||
_ = dbgen.TemplateVersionVariable(t, srv.Database, database.TemplateVersionVariable{
|
||||
TemplateVersionID: version.ID,
|
||||
Name: "first",
|
||||
Value: "first_value",
|
||||
DefaultValue: "default_value",
|
||||
Sensitive: true,
|
||||
})
|
||||
_ = dbgen.TemplateVersionVariable(t, srv.Database, database.TemplateVersionVariable{
|
||||
TemplateVersionID: version.ID,
|
||||
Name: "second",
|
||||
Value: "second_value",
|
||||
DefaultValue: "default_value",
|
||||
Required: true,
|
||||
Sensitive: false,
|
||||
})
|
||||
workspace := dbgen.Workspace(t, srv.Database, database.Workspace{
|
||||
TemplateID: template.ID,
|
||||
OwnerID: user.ID,
|
||||
@ -168,6 +187,17 @@ func TestAcquireJob(t *testing.T) {
|
||||
WorkspaceBuildId: build.ID.String(),
|
||||
WorkspaceName: workspace.Name,
|
||||
ParameterValues: []*sdkproto.ParameterValue{},
|
||||
VariableValues: []*sdkproto.VariableValue{
|
||||
{
|
||||
Name: "first",
|
||||
Value: "first_value",
|
||||
Sensitive: true,
|
||||
},
|
||||
{
|
||||
Name: "second",
|
||||
Value: "second_value",
|
||||
},
|
||||
},
|
||||
Metadata: &sdkproto.Provision_Metadata{
|
||||
CoderUrl: srv.AccessURL.String(),
|
||||
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
|
||||
@ -253,6 +283,49 @@ func TestAcquireJob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(want), string(got))
|
||||
})
|
||||
t.Run("TemplateVersionImportWithUserVariable", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := setup(t, false)
|
||||
|
||||
user := dbgen.User(t, srv.Database, database.User{})
|
||||
version := dbgen.TemplateVersion(t, srv.Database, database.TemplateVersion{})
|
||||
file := dbgen.File(t, srv.Database, database.File{CreatedBy: user.ID})
|
||||
_ = dbgen.ProvisionerJob(t, srv.Database, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
InitiatorID: user.ID,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{
|
||||
TemplateVersionID: version.ID,
|
||||
UserVariableValues: []codersdk.VariableValue{
|
||||
{Name: "first", Value: "first_value"},
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
job, err := srv.AcquireJob(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := json.Marshal(job.Type)
|
||||
require.NoError(t, err)
|
||||
|
||||
want, err := json.Marshal(&proto.AcquiredJob_TemplateImport_{
|
||||
TemplateImport: &proto.AcquiredJob_TemplateImport{
|
||||
UserVariableValues: []*sdkproto.VariableValue{
|
||||
{Name: "first", Value: "first_value"},
|
||||
},
|
||||
Metadata: &sdkproto.Provision_Metadata{
|
||||
CoderUrl: srv.AccessURL.String(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(want), string(got))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateJob(t *testing.T) {
|
||||
@ -384,6 +457,98 @@ func TestUpdateJob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "# hello world", version.Readme)
|
||||
})
|
||||
|
||||
t.Run("TemplateVariables", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
srv := setup(t, false)
|
||||
job := setupJob(t, srv)
|
||||
version, err := srv.Database.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
JobID: job,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
firstTemplateVariable := &sdkproto.TemplateVariable{
|
||||
Name: "first",
|
||||
Type: "string",
|
||||
DefaultValue: "default_value",
|
||||
Sensitive: true,
|
||||
}
|
||||
secondTemplateVariable := &sdkproto.TemplateVariable{
|
||||
Name: "second",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
}
|
||||
response, err := srv.UpdateJob(ctx, &proto.UpdateJobRequest{
|
||||
JobId: job.String(),
|
||||
TemplateVariables: []*sdkproto.TemplateVariable{
|
||||
firstTemplateVariable,
|
||||
secondTemplateVariable,
|
||||
},
|
||||
UserVariableValues: []*sdkproto.VariableValue{
|
||||
{
|
||||
Name: "second",
|
||||
Value: "foobar",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, response.VariableValues, 2)
|
||||
|
||||
templateVariables, err := srv.Database.GetTemplateVersionVariables(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateVariables, 2)
|
||||
require.Equal(t, templateVariables[0].Value, firstTemplateVariable.DefaultValue)
|
||||
require.Equal(t, templateVariables[1].Value, "foobar")
|
||||
})
|
||||
|
||||
t.Run("Missing required value", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
srv := setup(t, false)
|
||||
job := setupJob(t, srv)
|
||||
version, err := srv.Database.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
JobID: job,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
firstTemplateVariable := &sdkproto.TemplateVariable{
|
||||
Name: "first",
|
||||
Type: "string",
|
||||
DefaultValue: "default_value",
|
||||
Sensitive: true,
|
||||
}
|
||||
secondTemplateVariable := &sdkproto.TemplateVariable{
|
||||
Name: "second",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
}
|
||||
response, err := srv.UpdateJob(ctx, &proto.UpdateJobRequest{
|
||||
JobId: job.String(),
|
||||
TemplateVariables: []*sdkproto.TemplateVariable{
|
||||
firstTemplateVariable,
|
||||
secondTemplateVariable,
|
||||
},
|
||||
})
|
||||
require.Error(t, err) // required template variables need values
|
||||
require.Nil(t, response)
|
||||
|
||||
// Even though there is an error returned, variables are stored in the database
|
||||
// to show the schema in the site UI.
|
||||
templateVariables, err := srv.Database.GetTemplateVersionVariables(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateVariables, 2)
|
||||
require.Equal(t, templateVariables[0].Value, firstTemplateVariable.DefaultValue)
|
||||
require.Equal(t, templateVariables[1].Value, "")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailJob(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user