Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for Variables #29520

Merged
merged 25 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d20280e
feat: create and update user scope variable
sillyguodong Mar 1, 2024
7fb32af
chore: lint and typo
sillyguodong Mar 1, 2024
eacaba1
feat: delete and get user-level var
sillyguodong Mar 8, 2024
c0ccc4b
Merge branch 'main' into feat/api_for_variables
sillyguodong Mar 11, 2024
a4a0102
chore: generate swagger
sillyguodong Mar 11, 2024
ca218ad
feat: org-level var api
sillyguodong Mar 12, 2024
b23a836
feat: repo-level var api
sillyguodong Mar 12, 2024
e931674
revert Makefile
sillyguodong Mar 12, 2024
6e1548c
lint swagger
sillyguodong Mar 12, 2024
a4c633e
chore: add new line at end of swgger file
sillyguodong Mar 12, 2024
512d062
feat: list variables
sillyguodong Mar 13, 2024
73f483c
chore: tab to space
sillyguodong Mar 14, 2024
0dd5c00
Merge branch 'main' into feat/api_for_variables
sillyguodong Mar 14, 2024
473cff7
Merge branch 'main' into feat/api_for_variables
sillyguodong Mar 14, 2024
77624df
Merge branch 'main' into feat/api_for_variables
silverwind Mar 14, 2024
1806ad7
fix: no check null for owner_id and repo_id
sillyguodong Mar 25, 2024
ef5f9f5
Merge branch 'main' into feat/api_for_variables
sillyguodong Mar 26, 2024
bf807a6
chore: add integration test
sillyguodong Mar 26, 2024
da18636
chore: add comment for opts
sillyguodong Mar 26, 2024
6e21046
fix: misspelling
sillyguodong Mar 26, 2024
75d96f1
fix: test case
sillyguodong Mar 26, 2024
48f23c9
chore: mv func to util pkg
sillyguodong Mar 27, 2024
dec6da3
chore: re-run actions
sillyguodong Mar 27, 2024
a869bcc
Merge branch 'main' into feat/api_for_variables
techknowlogick Mar 28, 2024
1e3224b
Merge branch 'main' into feat/api_for_variables
GiteaBot Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions models/actions/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ type FindVariablesOpts struct {
db.ListOptions
OwnerID int64
RepoID int64
Name string
}

func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
if opts.OwnerID > 0 {
sillyguodong marked this conversation as resolved.
Show resolved Hide resolved
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
}
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.Name != "" {
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
}
return cond
}

Expand Down
21 changes: 21 additions & 0 deletions modules/structs/variable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package structs

// CreateVariableOption the option when creating variable
// swagger:model
type CreateVariableOption struct {
// Value of the variable to create
// required: true
Value string `json:"value" binding:"Required"`
}

// UpdateVariableOption the option when updating variable
type UpdateVariableOption struct {
// New name for the variable. If the field is empty, the variable name won't be updated.
Name string `json:"name"`
// Value of the variable to update
// required: true
Value string `json:"value" binding:"Required"`
}
7 changes: 7 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,13 @@ func Routes() *web.Route {
Delete(user.DeleteSecret)
})

m.Group("/variables", func() {
m.Combo("/{variablename}").
Post(bind(api.CreateVariableOption{}), user.CreateVariable).
sillyguodong marked this conversation as resolved.
Show resolved Hide resolved
Put(bind(api.UpdateVariableOption{}), user.UpdateVariable).
Delete(user.DeleteVariable)
})

m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
})
Expand Down
82 changes: 82 additions & 0 deletions routers/api/v1/user/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ package user

import (
"errors"
"fmt"
"net/http"

actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
Expand Down Expand Up @@ -101,3 +105,81 @@ func DeleteSecret(ctx *context.APIContext) {

ctx.Status(http.StatusNoContent)
}

// CreateVariable create a user-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation PUT /user/actions/variables/{variablename} user createUserVariable
// ---
// summary: Create a user-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariable"
// responses:
// "201":
// description: response when creating a variable
// "204":
// description: response when updating a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

opt := web.GetForm(ctx).(*api.CreateVariableOption)

if _, err := actions_service.CreateVariable(ctx, ctx.Doer.ID, 0, ctx.Params("variablename"), opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}

ctx.Status(http.StatusNoContent)
}

// UpdateVariable update a user-level variable
func UpdateVariable(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.UpdateVariableOption)

v, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Doer.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariable", err)
return
}
if len(v) == 0 {
ctx.Error(http.StatusNotFound, "FindVariable", fmt.Errorf("variable not found"))
return
}

if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v[0].ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}

ctx.Status(http.StatusNoContent)
}

// DeleteVariable delete a user-level variable
func DeleteVariable(ctx *context.APIContext) {}
67 changes: 7 additions & 60 deletions routers/web/shared/actions/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@
package actions

import (
"errors"
"regexp"
"strings"

actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
)

func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
Expand All @@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Variables"] = variables
}

// some regular expression of `variables` and `secrets`
// reference to:
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)

func envNameCIRegexMatch(name string) error {
if forbiddenEnvNameCIRx.MatchString(name) {
log.Error("Env Name cannot be ci")
return errors.New("env name cannot be ci")
}
return nil
}

func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.EditVariableForm)

if err := secret_service.ValidateName(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}

if err := envNameCIRegexMatch(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}

v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
if err != nil {
log.Error("InsertVariable error: %v", err)
log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}

ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
ctx.JSONRedirect(redirectURL)
}
Expand All @@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
form := web.GetForm(ctx).(*forms.EditVariableForm)

if err := secret_service.ValidateName(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}

if err := envNameCIRegexMatch(form.Name); err != nil {
ctx.JSONError(err.Error())
return
}

ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
ID: id,
Name: strings.ToUpper(form.Name),
Data: ReserveLineBreakForTextarea(form.Data),
})
if err != nil || !ok {
log.Error("UpdateVariable error: %v", err)
if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
Expand All @@ -99,20 +55,11 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
func DeleteVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")

if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
if _, err := actions_service.DeleteVariable(ctx, id); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
ctx.JSONRedirect(redirectURL)
}

func ReserveLineBreakForTextarea(input string) string {
// Since the content is from a form which is a textarea, the line endings are \r\n.
// It's a standard behavior of HTML.
// But we want to store them as \n like what GitHub does.
// And users are unlikely to really need to keep the \r.
// Other than this, we should respect the original content, even leading or trailing spaces.
return strings.ReplaceAll(input, "\r\n", "\n")
}
4 changes: 2 additions & 2 deletions routers/web/shared/secrets/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/shared/actions"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
Expand All @@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)

s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions_service.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
Expand Down
78 changes: 78 additions & 0 deletions services/actions/variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"context"
"regexp"
"strings"

actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
secret_service "code.gitea.io/gitea/services/secrets"
)

func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
if err := secret_service.ValidateName(name); err != nil {
return nil, err
}

if err := envNameCIRegexMatch(name); err != nil {
return nil, err
}

v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, ReserveLineBreakForTextarea(data))
if err != nil {
return nil, err
}

return v, nil
}

func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
if err := secret_service.ValidateName(name); err != nil {
return false, err
}

if err := envNameCIRegexMatch(name); err != nil {
return false, err
}

return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
ID: variableID,
Name: strings.ToUpper(name),
Data: ReserveLineBreakForTextarea(data),
})
}

func DeleteVariable(ctx context.Context, variableID int64) (int64, error) {
return db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: variableID})
}

// some regular expression of `variables` and `secrets`
// reference to:
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)

func envNameCIRegexMatch(name string) error {
if forbiddenEnvNameCIRx.MatchString(name) {
log.Error("Env Name cannot be ci")
return util.NewInvalidArgumentErrorf("env name cannot be ci")
}
return nil
}

func ReserveLineBreakForTextarea(input string) string {
// Since the content is from a form which is a textarea, the line endings are \r\n.
// It's a standard behavior of HTML.
// But we want to store them as \n like what GitHub does.
// And users are unlikely to really need to keep the \r.
// Other than this, we should respect the original content, even leading or trailing spaces.
return strings.ReplaceAll(input, "\r\n", "\n")
}
sillyguodong marked this conversation as resolved.
Show resolved Hide resolved
Loading