mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: archive template versions to hide them from the ui (#10179)
* api + cli implementation
This commit is contained in:
129
coderd/apidoc/docs.go
generated
129
coderd/apidoc/docs.go
generated
@ -2365,6 +2365,53 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions/archive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Archive template unused versions by template id",
|
||||
"operationId": "archive-template-unused-versions-by-template-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template ID",
|
||||
"name": "template",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Archive request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ArchiveTemplateVersionsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions/{templateversionname}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -2490,6 +2537,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/archive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Archive template version",
|
||||
"operationId": "archive-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/cancel": {
|
||||
"patch": {
|
||||
"security": [
|
||||
@ -2996,6 +3078,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/unarchive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Unarchive template version",
|
||||
"operationId": "unarchive-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/variables": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -7145,6 +7262,15 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ArchiveTemplateVersionsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all": {
|
||||
"description": "By default, only failed versions are archived. Set this to true\nto archive all unused versions regardless of job status.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AssignableRoles": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -10087,6 +10213,9 @@ const docTemplate = `{
|
||||
"codersdk.TemplateVersion": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
115
coderd/apidoc/swagger.json
generated
115
coderd/apidoc/swagger.json
generated
@ -2067,6 +2067,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions/archive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Archive template unused versions by template id",
|
||||
"operationId": "archive-template-unused-versions-by-template-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template ID",
|
||||
"name": "template",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Archive request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ArchiveTemplateVersionsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions/{templateversionname}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -2178,6 +2219,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/archive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Archive template version",
|
||||
"operationId": "archive-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/cancel": {
|
||||
"patch": {
|
||||
"security": [
|
||||
@ -2638,6 +2710,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/unarchive": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Unarchive template version",
|
||||
"operationId": "unarchive-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/variables": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -6347,6 +6450,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ArchiveTemplateVersionsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all": {
|
||||
"description": "By default, only failed versions are archived. Set this to true\nto archive all unused versions regardless of job status.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AssignableRoles": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9122,6 +9234,9 @@
|
||||
"codersdk.TemplateVersion": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
@ -670,6 +670,7 @@ func New(options *Options) *API {
|
||||
r.Delete("/", api.deleteTemplate)
|
||||
r.Patch("/", api.patchTemplateMeta)
|
||||
r.Route("/versions", func(r chi.Router) {
|
||||
r.Post("/archive", api.postArchiveTemplateVersions)
|
||||
r.Get("/", api.templateVersionsByTemplate)
|
||||
r.Patch("/", api.patchActiveTemplateVersion)
|
||||
r.Get("/{templateversionname}", api.templateVersionByName)
|
||||
@ -683,6 +684,8 @@ func New(options *Options) *API {
|
||||
r.Get("/", api.templateVersion)
|
||||
r.Patch("/", api.patchTemplateVersion)
|
||||
r.Patch("/cancel", api.patchCancelTemplateVersion)
|
||||
r.Post("/archive", api.postArchiveTemplateVersion())
|
||||
r.Post("/unarchive", api.postUnarchiveTemplateVersion())
|
||||
// Old agents may expect a non-error response from /schema and /parameters endpoints.
|
||||
// The idea is to return an empty [], so that the coder CLI won't get blocked accidentally.
|
||||
r.Get("/schema", templateVersionSchemaDeprecated)
|
||||
|
@ -5345,6 +5345,8 @@ func (q *FakeQuerier) UnarchiveTemplateVersion(_ context.Context, arg database.U
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, v := range q.data.templateVersions {
|
||||
if v.ID == arg.TemplateVersionID {
|
||||
|
@ -79,6 +79,17 @@ func (p *QueryParamParser) Int(vals url.Values, def int, queryParam string) int
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *QueryParamParser) Boolean(vals url.Values, def bool, queryParam string) bool {
|
||||
v, err := parseQueryParam(p, vals, strconv.ParseBool, def, queryParam)
|
||||
if err != nil {
|
||||
p.Errors = append(p.Errors, codersdk.ValidationError{
|
||||
Field: queryParam,
|
||||
Detail: fmt.Sprintf("Query param %q must be a valid boolean (%s)", queryParam, err.Error()),
|
||||
})
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *QueryParamParser) Required(queryParam string) *QueryParamParser {
|
||||
p.RequiredParams[queryParam] = true
|
||||
return p
|
||||
|
@ -157,6 +157,48 @@ func TestParseQueryParams(t *testing.T) {
|
||||
testQueryParams(t, expParams, parser, parser.String)
|
||||
})
|
||||
|
||||
t.Run("Boolean", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
expParams := []queryParamTestCase[bool]{
|
||||
{
|
||||
QueryParam: "valid_true",
|
||||
Value: "true",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
QueryParam: "casing",
|
||||
Value: "True",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
QueryParam: "all_caps",
|
||||
Value: "TRUE",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
QueryParam: "no_value_true_def",
|
||||
NoSet: true,
|
||||
Default: true,
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
QueryParam: "no_value",
|
||||
NoSet: true,
|
||||
Expected: false,
|
||||
},
|
||||
|
||||
{
|
||||
QueryParam: "invalid_boolean",
|
||||
Value: "yes",
|
||||
Expected: false,
|
||||
ExpectedErrorContains: "must be a valid boolean",
|
||||
},
|
||||
}
|
||||
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
testQueryParams(t, expParams, parser, parser.Boolean)
|
||||
})
|
||||
|
||||
t.Run("Int", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
expParams := []queryParamTestCase[int]{
|
||||
|
@ -193,6 +193,15 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
})
|
||||
return
|
||||
}
|
||||
if templateVersion.Archived {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Template version %s is archived.", createTemplate.VersionID),
|
||||
Validations: []codersdk.ValidationError{
|
||||
{Field: "template_version_id", Detail: "Template version is archived"},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersionAudit.Old = templateVersion
|
||||
if templateVersion.TemplateID.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
|
@ -717,6 +717,17 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// If this throws an error, the boolean is false. Which is the default we want.
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
includeArchived := parser.Boolean(r.URL.Query(), false, "include_archived")
|
||||
if len(parser.Errors) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid query parameters.",
|
||||
Validations: parser.Errors,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
apiVersions := []codersdk.TemplateVersion{}
|
||||
err = api.Database.InTx(func(store database.Store) error {
|
||||
@ -738,11 +749,21 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude archived templates versions
|
||||
archiveFilter := sql.NullBool{
|
||||
Bool: false,
|
||||
Valid: true,
|
||||
}
|
||||
if includeArchived {
|
||||
archiveFilter = sql.NullBool{Valid: false}
|
||||
}
|
||||
|
||||
versions, err := store.GetTemplateVersionsByTemplateID(ctx, database.GetTemplateVersionsByTemplateIDParams{
|
||||
TemplateID: template.ID,
|
||||
AfterID: paginationParams.AfterID,
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
Archived: archiveFilter,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, apiVersions)
|
||||
@ -991,6 +1012,173 @@ func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.Res
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), nil))
|
||||
}
|
||||
|
||||
// @Summary Archive template unused versions by template id
|
||||
// @ID archive-template-unused-versions-by-template-id
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param template path string true "Template ID" format(uuid)
|
||||
// @Param request body codersdk.ArchiveTemplateVersionsRequest true "Archive request"
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /templates/{template}/versions/archive [post]
|
||||
func (api *API) postArchiveTemplateVersions(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
template = httpmw.TemplateParam(r)
|
||||
auditor = *api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
|
||||
Audit: auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
aReq.Old = template
|
||||
|
||||
var req codersdk.ArchiveTemplateVersionsRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
status := database.NullProvisionerJobStatus{
|
||||
ProvisionerJobStatus: database.ProvisionerJobStatusFailed,
|
||||
Valid: true,
|
||||
}
|
||||
if req.All {
|
||||
status = database.NullProvisionerJobStatus{}
|
||||
}
|
||||
|
||||
archived, err := api.Database.ArchiveUnusedTemplateVersions(ctx, database.ArchiveUnusedTemplateVersionsParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateID: template.ID,
|
||||
JobStatus: status,
|
||||
// Archive all versions that match
|
||||
TemplateVersionID: uuid.Nil,
|
||||
})
|
||||
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Template or template versions not found.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ArchiveTemplateVersionsResponse{
|
||||
TemplateID: template.ID,
|
||||
ArchivedIDs: archived,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Archive template version
|
||||
// @ID archive-template-version
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /templateversions/{templateversion}/archive [post]
|
||||
func (api *API) postArchiveTemplateVersion() func(rw http.ResponseWriter, r *http.Request) {
|
||||
return api.setArchiveTemplateVersion(true)
|
||||
}
|
||||
|
||||
// @Summary Unarchive template version
|
||||
// @ID unarchive-template-version
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /templateversions/{templateversion}/unarchive [post]
|
||||
func (api *API) postUnarchiveTemplateVersion() func(rw http.ResponseWriter, r *http.Request) {
|
||||
return api.setArchiveTemplateVersion(false)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func (api *API) setArchiveTemplateVersion(archive bool) func(rw http.ResponseWriter, r *http.Request) {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
templateVersion = httpmw.TemplateVersionParam(r)
|
||||
auditor = *api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.TemplateVersion](rw, &audit.RequestParams{
|
||||
Audit: auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
aReq.Old = templateVersion
|
||||
|
||||
verb := "archived"
|
||||
if !archive {
|
||||
verb = "unarchived"
|
||||
}
|
||||
if templateVersion.Archived == archive {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Template version already %s", verb),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !templateVersion.TemplateID.Valid {
|
||||
// Maybe we should allow this?
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cannot archive template versions not associate with a template.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if archive {
|
||||
archived, archiveError := api.Database.ArchiveUnusedTemplateVersions(ctx, database.ArchiveUnusedTemplateVersionsParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateID: templateVersion.TemplateID.UUID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
JobStatus: database.NullProvisionerJobStatus{},
|
||||
})
|
||||
|
||||
if archiveError != nil {
|
||||
err = archiveError
|
||||
} else {
|
||||
if len(archived) == 0 {
|
||||
err = xerrors.New("Unable to archive specified version, the version is likely in use by a workspace or currently set to the active version")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = api.Database.UnarchiveTemplateVersion(ctx, database.UnarchiveTemplateVersionParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Template or template versions not found.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, fmt.Sprintf("template version %q %s", templateVersion.ID.String(), verb))
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Update active template version by template ID
|
||||
// @ID update-active-template-version-by-template-id
|
||||
// @Security CoderSessionToken
|
||||
@ -1055,6 +1243,12 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
|
||||
})
|
||||
return
|
||||
}
|
||||
if version.Archived {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "The provided template version is archived.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.InTx(func(store database.Store) error {
|
||||
err = store.UpdateTemplateActiveVersionByID(ctx, database.UpdateTemplateActiveVersionByIDParams{
|
||||
@ -1404,6 +1598,7 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi
|
||||
Username: version.CreatedByUsername,
|
||||
AvatarURL: version.CreatedByAvatarURL.String,
|
||||
},
|
||||
Archived: version.Archived,
|
||||
Warnings: warnings,
|
||||
}
|
||||
}
|
||||
|
@ -619,6 +619,34 @@ func TestPatchActiveTemplateVersion(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Archived", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
auditor := audit.NewMock()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
Auditor: auditor,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
version = coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, nil, template.ID)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
err := client.SetArchiveTemplateVersion(ctx, version.ID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "The provided template version is archived")
|
||||
})
|
||||
|
||||
t.Run("SuccessfulBuild", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
auditor := audit.NewMock()
|
||||
@ -1515,3 +1543,118 @@ func TestTemplateVersionParameters_Order(t *testing.T) {
|
||||
require.Equal(t, secondParameterName, templateRichParameters[3].Name)
|
||||
require.Equal(t, thirdParameterName, templateRichParameters[4].Name)
|
||||
}
|
||||
|
||||
func TestTemplateArchiveVersions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
var totalVersions int
|
||||
// Create a template to archive
|
||||
initialVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
totalVersions++
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, initialVersion.ID)
|
||||
|
||||
allFailed := make([]uuid.UUID, 0)
|
||||
expArchived := make([]uuid.UUID, 0)
|
||||
// create some failed versions
|
||||
for i := 0; i < 2; i++ {
|
||||
failed := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanFailed,
|
||||
ProvisionApply: echo.ApplyFailed,
|
||||
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
req.TemplateID = template.ID
|
||||
})
|
||||
allFailed = append(allFailed, failed.ID)
|
||||
totalVersions++
|
||||
}
|
||||
|
||||
// Create some unused versions
|
||||
for i := 0; i < 2; i++ {
|
||||
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
req.TemplateID = template.ID
|
||||
})
|
||||
expArchived = append(expArchived, unused.ID)
|
||||
totalVersions++
|
||||
}
|
||||
|
||||
// Create some used template versions
|
||||
for i := 0; i < 2; i++ {
|
||||
used := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
req.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, used.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
||||
request.TemplateVersionID = used.ID
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
totalVersions++
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 100,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "fetch all versions")
|
||||
require.Len(t, versions, totalVersions, "total versions")
|
||||
|
||||
// Archive failed versions
|
||||
archiveFailed, err := client.ArchiveTemplateVersions(ctx, template.ID, false)
|
||||
require.NoError(t, err, "archive failed versions")
|
||||
require.ElementsMatch(t, archiveFailed.ArchivedIDs, allFailed, "all failed versions archived")
|
||||
|
||||
remaining, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 100,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "fetch all non-failed versions")
|
||||
require.Len(t, remaining, totalVersions-len(allFailed), "remaining non-failed versions")
|
||||
|
||||
// Try archiving "All" unused templates
|
||||
archived, err := client.ArchiveTemplateVersions(ctx, template.ID, true)
|
||||
require.NoError(t, err, "archive versions")
|
||||
require.ElementsMatch(t, archived.ArchivedIDs, expArchived, "all expected versions archived")
|
||||
|
||||
remaining, err = client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 100,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "fetch all versions")
|
||||
require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed), "remaining versions")
|
||||
|
||||
// Unarchive a version
|
||||
err = client.SetArchiveTemplateVersion(ctx, expArchived[0], false)
|
||||
require.NoError(t, err, "unarchive a version")
|
||||
|
||||
tv, err := client.TemplateVersion(ctx, expArchived[0])
|
||||
require.NoError(t, err, "fetch version")
|
||||
require.False(t, tv.Archived, "expect unarchived")
|
||||
|
||||
// Check the remaining again
|
||||
remaining, err = client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 100,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "fetch all versions")
|
||||
require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed)+1, "remaining versions")
|
||||
}
|
||||
|
@ -352,6 +352,18 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
})
|
||||
return
|
||||
}
|
||||
if templateVersion.Archived {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Archived template versions cannot be used to make a workspace.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{
|
||||
Field: "template_version_id",
|
||||
Detail: "template version archived",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
templateID = templateVersion.TemplateID.UUID
|
||||
}
|
||||
|
@ -308,6 +308,36 @@ func TestWorkspace(t *testing.T) {
|
||||
assert.NotEmpty(t, agent2.Health.Reason)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Archived", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
active := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, active.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, active.ID)
|
||||
// We need another version because the active template version cannot be
|
||||
// archived.
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
|
||||
request.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
err := client.SetArchiveTemplateVersion(ctx, version.ID, true)
|
||||
require.NoError(t, err, "archive version")
|
||||
|
||||
_, err = client.CreateWorkspace(ctx, owner.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateVersionID: version.ID,
|
||||
Name: "testworkspace",
|
||||
})
|
||||
require.Error(t, err, "create workspace with archived version")
|
||||
require.ErrorContains(t, err, "Archived template versions cannot")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user