Compare commits
22 Commits
devstar-do
...
mergeDevCo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364a8fad8f | ||
|
|
d0f9cd5ef0 | ||
|
|
5caf41cf28 | ||
|
|
2e7927a23e | ||
|
|
79ace0ac38 | ||
|
|
6e9f6a829d | ||
|
|
83085dc2d0 | ||
|
|
a039f17913 | ||
|
|
895b10cc2f | ||
|
|
fd532af745 | ||
|
|
6ff9b9c665 | ||
|
|
7bf829b599 | ||
|
|
a19272de73 | ||
|
|
51820bf0eb | ||
|
|
6686a44316 | ||
|
|
7f86efe563 | ||
|
|
de6fd128cf | ||
|
|
41955ed427 | ||
|
|
0e9b1020d9 | ||
|
|
923816b0b9 | ||
|
|
db9f69958b | ||
|
|
52b2fce7b0 |
23
Makefile
23
Makefile
@@ -917,31 +917,12 @@ generate-manpage: ## generate manpage
|
|||||||
|
|
||||||
.PHONY: devstar
|
.PHONY: devstar
|
||||||
devstar:
|
devstar:
|
||||||
@if docker pull devstar.cn/devstar/devstar-dev-container:v1.0; then \
|
|
||||||
docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest && \
|
|
||||||
echo "Successfully pulled devstar.cn/devstar/devstar-dev-container:v1.0 taged to latest"; \
|
|
||||||
else \
|
|
||||||
docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer . && \
|
|
||||||
echo "Successfully build devstar.cn/devstar/devstar-dev-container:latest"; \
|
|
||||||
fi
|
|
||||||
@if docker pull devstar.cn/devstar/devstar-runtime-container:v1.0; then \
|
|
||||||
docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest && \
|
|
||||||
echo "Successfully pulled devstar.cn/devstar/devstar-runtime-container:v1.0 taged to latest"; \
|
|
||||||
else \
|
|
||||||
docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer . && \
|
|
||||||
echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \
|
|
||||||
fi
|
|
||||||
@if docker pull devstar.cn/devstar/webterminal:v1.0; then \
|
|
||||||
docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest && \
|
|
||||||
echo "Successfully pulled devstar.cn/devstar/webterminal:v1.0 taged to latest"; \
|
|
||||||
else \
|
|
||||||
docker build --no-cache -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal . && \
|
|
||||||
echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \
|
|
||||||
fi
|
|
||||||
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
|
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
|
||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker:
|
docker:
|
||||||
|
docker build -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal .
|
||||||
|
|
||||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||||
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ After building, a binary file named `gitea` will be generated in the root of the
|
|||||||
./gitea web
|
./gitea web
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> devcontainer相关功能不能在localhost域名下正常工作,调试环境请在custom/conf/app.ini中修改为IP地址
|
|
||||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
||||||
|
|
||||||
Start from Container Image:
|
Start from Container Image:
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ RUN apk --no-cache add \
|
|||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# To acquire Gitea dev container:
|
# To acquire Gitea dev container:
|
||||||
# $ docker build -t devstar.cn/devstar/devstar-dev-container:v1.0 -f docker/Dockerfile.devContainer .
|
# $ docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer .
|
||||||
# $ docker login devstar.cn
|
# $ docker login devstar.cn
|
||||||
# $ docker push devstar.cn/devstar/devstar-dev-container:v1.0
|
|
||||||
# $ docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest
|
|
||||||
# $ docker push devstar.cn/devstar/devstar-dev-container:latest
|
# $ docker push devstar.cn/devstar/devstar-dev-container:latest
|
||||||
|
|
||||||
|
|
||||||
# Release Notes:
|
|
||||||
# v1.0 - Initial release
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ RUN chown git:git /var/lib/gitea /etc/gitea
|
|||||||
COPY --from=build-env /tmp/local /
|
COPY --from=build-env /tmp/local /
|
||||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
||||||
|
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/webTerminal.sh /app/gitea/webTerminal.sh
|
||||||
# git:git
|
# git:git
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
ENV GITEA_WORK_DIR=/var/lib/gitea
|
ENV GITEA_WORK_DIR=/var/lib/gitea
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ RUN apk --no-cache add \
|
|||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# To acquire Gitea base runtime container:
|
# To acquire Gitea base runtime container:
|
||||||
# $ docker build -t devstar.cn/devstar/devstar-runtime-container:v1.0 -f docker/Dockerfile.runtimeContainer .
|
# $ docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer .
|
||||||
# $ docker login devstar.cn
|
# $ docker login devstar.cn
|
||||||
# $ docker push devstar.cn/devstar/devstar-runtime-container:v1.0
|
|
||||||
# $ docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest
|
|
||||||
# $ docker push devstar.cn/devstar/devstar-runtime-container:latest
|
# $ docker push devstar.cn/devstar/devstar-runtime-container:latest
|
||||||
|
|
||||||
|
|
||||||
# Release Notes:
|
|
||||||
# v1.0 - Initial release
|
|
||||||
|
|||||||
@@ -38,13 +38,3 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["/home/webTerminal/build/ttyd", "-W", "bash"]
|
CMD ["/home/webTerminal/build/ttyd", "-W", "bash"]
|
||||||
|
|
||||||
# To acquire devstar.cn/devstar/webterminal:latest:
|
|
||||||
# $ docker build --no-cache -t devstar.cn/devstar/webterminal:v1.0 -f docker/Dockerfile.webTerminal .
|
|
||||||
# $ docker login devstar.cn
|
|
||||||
# $ docker push devstar.cn/devstar/webterminal:v1.0
|
|
||||||
# $ docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest
|
|
||||||
# $ docker push devstar.cn/devstar/webterminal:latest
|
|
||||||
|
|
||||||
# Release Notes:
|
|
||||||
# v1.0 - Initial release https://devstar.cn/devstar/webTerminal/commit/2bf050cff984d6e64c4f9753d64e1124fc152ad7
|
|
||||||
2
go.mod
2
go.mod
@@ -40,7 +40,6 @@ require (
|
|||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||||
github.com/djherbis/buffer v1.2.0
|
github.com/djherbis/buffer v1.2.0
|
||||||
github.com/djherbis/nio/v3 v3.0.1
|
github.com/djherbis/nio/v3 v3.0.1
|
||||||
github.com/docker/go-connections v0.4.0
|
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
||||||
@@ -140,6 +139,7 @@ require (
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/distribution/reference v0.5.0 // indirect
|
github.com/distribution/reference v0.5.0 // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
package devcontainer
|
package devcontainer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Devcontainer struct {
|
type Devcontainer struct {
|
||||||
@@ -35,122 +29,7 @@ type DevcontainerOutput struct {
|
|||||||
DevcontainerId int64 `xorm:"BIGINT NOT NULL FK('devcontainer_id') REFERENCES devcontainer(id) ON DELETE CASCADE 'devcontainer_id' comment('devcontainer表主键')"`
|
DevcontainerId int64 `xorm:"BIGINT NOT NULL FK('devcontainer_id') REFERENCES devcontainer(id) ON DELETE CASCADE 'devcontainer_id' comment('devcontainer表主键')"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DevcontainerScript struct {
|
|
||||||
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键,devContainerId')"`
|
|
||||||
RepoId int64 `xorm:"BIGINT NOT NULL 'repo_id' comment('repository表主键')"`
|
|
||||||
UserId int64 `xorm:"BIGINT NOT NULL 'user_id' comment('user表主键')"`
|
|
||||||
VariableName string `xorm:"NOT NULL 'variable_name' comment('user表主键')"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
db.RegisterModel(new(Devcontainer))
|
db.RegisterModel(new(Devcontainer))
|
||||||
db.RegisterModel(new(DevcontainerScript))
|
|
||||||
db.RegisterModel(new(DevcontainerOutput))
|
db.RegisterModel(new(DevcontainerOutput))
|
||||||
}
|
}
|
||||||
func GetScript(ctx context.Context, userId, repoID int64) (map[string]string, error) {
|
|
||||||
variables := make(map[string]string)
|
|
||||||
var devstarVariables []*DevcontainerVariable
|
|
||||||
var name []string
|
|
||||||
// Devstar level
|
|
||||||
// 从远程获取Devstar变量
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create request for devstar variables: %v", err)
|
|
||||||
} else {
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to fetch devstar variables: %v", err)
|
|
||||||
} else {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read devstar variables response: %v", err)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &devstarVariables)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to unmarshal devstar variables: %v", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Select("variable_name").
|
|
||||||
Table("devcontainer_script").
|
|
||||||
Where("user_id = ? AND repo_id = ?", 0, 0).
|
|
||||||
Find(&name)
|
|
||||||
|
|
||||||
globalVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find global variables: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 过滤出name在variableNames中的变量
|
|
||||||
globalVariables = append(devstarVariables, globalVariables...)
|
|
||||||
var filteredGlobalVars []*DevcontainerVariable
|
|
||||||
for _, v := range globalVariables {
|
|
||||||
if contains(name, v.Name) {
|
|
||||||
filteredGlobalVars = append(filteredGlobalVars, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Org / User level
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Select("variable_name").
|
|
||||||
Table("devcontainer_script").
|
|
||||||
Where("user_id = ? AND repo_id = ?", userId, 0).
|
|
||||||
Find(&name)
|
|
||||||
ownerVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{OwnerID: userId})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find variables of org: %d, error: %v", userId, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 过滤出name在variableNames中的变量
|
|
||||||
ownerVariables = append(devstarVariables, ownerVariables...)
|
|
||||||
var filteredOwnerVars []*DevcontainerVariable
|
|
||||||
for _, v := range ownerVariables {
|
|
||||||
if contains(name, v.Name) {
|
|
||||||
filteredOwnerVars = append(filteredOwnerVars, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Repo level
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Select("variable_name").
|
|
||||||
Table("devcontainer_script").
|
|
||||||
Where("repo_id = ?", repoID).
|
|
||||||
Find(&name)
|
|
||||||
repoVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{RepoID: repoID})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find variables of repo: %d, error: %v", repoID, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 过滤出name在variableNames中的变量
|
|
||||||
repoVariables = append(devstarVariables, repoVariables...)
|
|
||||||
var filteredRepoVars []*DevcontainerVariable
|
|
||||||
for _, v := range repoVariables {
|
|
||||||
if contains(name, v.Name) {
|
|
||||||
filteredRepoVars = append(filteredRepoVars, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Level precedence: Org / User > Repo > Global
|
|
||||||
for _, v := range append(filteredGlobalVars, append(filteredRepoVars, filteredOwnerVars...)...) {
|
|
||||||
variables[v.Name] = v.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
return variables, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// contains 检查字符串切片中是否包含指定的字符串
|
|
||||||
func contains(slice []string, item string) bool {
|
|
||||||
for _, s := range slice {
|
|
||||||
if s == item {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package devcontainer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DevcontainerVariable represents a variable that can be used in actions
|
|
||||||
//
|
|
||||||
// It can be:
|
|
||||||
// 1. global variable, OwnerID is 0 and RepoID is 0
|
|
||||||
// 2. org/user level variable, OwnerID is org/user ID and RepoID is 0
|
|
||||||
// 3. repo level variable, OwnerID is 0 and RepoID is repo ID
|
|
||||||
//
|
|
||||||
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
|
|
||||||
// or it will be complicated to find variables belonging to a specific owner.
|
|
||||||
// For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1},
|
|
||||||
// but it's a repo level variable, not an org/user level variable.
|
|
||||||
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables.
|
|
||||||
type DevcontainerVariable struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
|
|
||||||
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
|
|
||||||
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
|
|
||||||
Data string `xorm:"LONGTEXT NOT NULL"`
|
|
||||||
Description string `xorm:"TEXT"`
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
VariableDescriptionMaxLength = 4096
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(DevcontainerVariable))
|
|
||||||
}
|
|
||||||
|
|
||||||
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*DevcontainerVariable, error) {
|
|
||||||
if ownerID != 0 && repoID != 0 {
|
|
||||||
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
|
|
||||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
|
||||||
ownerID = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
description = util.TruncateRunes(description, VariableDescriptionMaxLength)
|
|
||||||
|
|
||||||
variable := &DevcontainerVariable{
|
|
||||||
OwnerID: ownerID,
|
|
||||||
RepoID: repoID,
|
|
||||||
Name: strings.ToUpper(name),
|
|
||||||
Data: data,
|
|
||||||
Description: description,
|
|
||||||
}
|
|
||||||
return variable, db.Insert(ctx, variable)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindVariablesOpts struct {
|
|
||||||
db.ListOptions
|
|
||||||
IDs []int64
|
|
||||||
RepoID int64
|
|
||||||
OwnerID int64 // it will be ignored if RepoID is set
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opts FindVariablesOpts) ToConds() builder.Cond {
|
|
||||||
cond := builder.NewCond()
|
|
||||||
|
|
||||||
if len(opts.IDs) > 0 {
|
|
||||||
if len(opts.IDs) == 1 {
|
|
||||||
cond = cond.And(builder.Eq{"id": opts.IDs[0]})
|
|
||||||
} else {
|
|
||||||
cond = cond.And(builder.In("id", opts.IDs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we now support instance-level variables,
|
|
||||||
// there is no need to check for null values for `owner_id` and `repo_id`
|
|
||||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
|
||||||
if opts.RepoID != 0 { // if RepoID is set
|
|
||||||
// ignore OwnerID and treat it as 0
|
|
||||||
cond = cond.And(builder.Eq{"owner_id": 0})
|
|
||||||
} else {
|
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Name != "" {
|
|
||||||
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
|
|
||||||
}
|
|
||||||
return cond
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*DevcontainerVariable, error) {
|
|
||||||
return db.Find[DevcontainerVariable](ctx, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateVariableCols(ctx context.Context, variable *DevcontainerVariable, cols ...string) (bool, error) {
|
|
||||||
|
|
||||||
variable.Description = util.TruncateRunes(variable.Description, VariableDescriptionMaxLength)
|
|
||||||
|
|
||||||
variable.Name = strings.ToUpper(variable.Name)
|
|
||||||
count, err := db.GetEngine(ctx).
|
|
||||||
ID(variable.ID).
|
|
||||||
Cols(cols...).
|
|
||||||
Update(variable)
|
|
||||||
return count != 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteVariable(ctx context.Context, id int64) error {
|
|
||||||
if _, err := db.DeleteByID[DevcontainerVariable](ctx, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetVariables(ctx context.Context, userId, repoID int64) (map[string]string, error) {
|
|
||||||
variables := map[string]string{}
|
|
||||||
|
|
||||||
// Global
|
|
||||||
globalVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find global variables: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Org / User level
|
|
||||||
ownerVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{OwnerID: userId})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find variables of org: %d, error: %v", userId, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo level
|
|
||||||
repoVariables, err := db.Find[DevcontainerVariable](ctx, FindVariablesOpts{RepoID: repoID})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("find variables of repo: %d, error: %v", repoID, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Level precedence: Org / User > Repo > Global
|
|
||||||
for _, v := range append(globalVariables, append(repoVariables, ownerVariables...)...) {
|
|
||||||
variables[v.Name] = v.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
return variables, nil
|
|
||||||
}
|
|
||||||
@@ -667,7 +667,6 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
|
|||||||
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||||
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
||||||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
||||||
u.AllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
|
||||||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
||||||
u.MaxRepoCreation = -1
|
u.MaxRepoCreation = -1
|
||||||
u.Theme = setting.UI.DefaultTheme
|
u.Theme = setting.UI.DefaultTheme
|
||||||
|
|||||||
@@ -89,22 +89,24 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
state := containerInfo.State
|
state := containerInfo.State
|
||||||
return state.Status, nil
|
return state.Status, nil
|
||||||
}
|
}
|
||||||
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
||||||
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
||||||
cmd := exec.Command("sh", "-c", script)
|
cmd := exec.Command("sh", "-c", script)
|
||||||
output, err := cmd.CombinedOutput()
|
_, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
|
return err
|
||||||
}
|
}
|
||||||
// 推送到仓库
|
// 推送到仓库
|
||||||
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
||||||
cmd = exec.Command("sh", "-c", script)
|
cmd = exec.Command("sh", "-c", script)
|
||||||
output, err = cmd.CombinedOutput()
|
_, err = cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ var Service = struct {
|
|||||||
McaptchaURL string
|
McaptchaURL string
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
DefaultAllowCreateDevcontainer bool
|
|
||||||
DefaultUserIsRestricted bool
|
DefaultUserIsRestricted bool
|
||||||
EnableTimetracking bool
|
EnableTimetracking bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
@@ -206,7 +205,6 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
|||||||
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
||||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||||
Service.DefaultAllowCreateDevcontainer = sec.Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").MustBool(true)
|
|
||||||
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||||
if Service.EnableTimetracking {
|
if Service.EnableTimetracking {
|
||||||
|
|||||||
@@ -362,9 +362,7 @@ invalid_log_root_path = The log path is invalid: %v
|
|||||||
default_keep_email_private = Hide Email Addresses by Default
|
default_keep_email_private = Hide Email Addresses by Default
|
||||||
default_keep_email_private_popup = Hide email addresses of new user accounts by default.
|
default_keep_email_private_popup = Hide email addresses of new user accounts by default.
|
||||||
default_allow_create_organization = Allow Creation of Organizations by Default
|
default_allow_create_organization = Allow Creation of Organizations by Default
|
||||||
default_allow_create_devcontainer = Allow Creation of DevContainers by Default
|
|
||||||
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
||||||
default_allow_create_devcontainer_popup = Allow new user accounts to create devcontainers by default.
|
|
||||||
default_enable_timetracking = Enable Time Tracking by Default
|
default_enable_timetracking = Enable Time Tracking by Default
|
||||||
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
||||||
no_reply_address = Hidden Email Domain
|
no_reply_address = Hidden Email Domain
|
||||||
@@ -1074,11 +1072,8 @@ visibility.private_tooltip = Visible only to members of organizations you have j
|
|||||||
dev_container = Dev Container
|
dev_container = Dev Container
|
||||||
dev_container_empty = Oops, it looks like there is no Dev Container Setting in this repository.
|
dev_container_empty = Oops, it looks like there is no Dev Container Setting in this repository.
|
||||||
dev_container_invalid_config_prompt = Invalid Dev Container Configuration: Please upload a valid 'devcontainer.json' file to the default branch, and ensure that this repository is NOT archived.
|
dev_container_invalid_config_prompt = Invalid Dev Container Configuration: Please upload a valid 'devcontainer.json' file to the default branch, and ensure that this repository is NOT archived.
|
||||||
dev_container_control = Container Management
|
|
||||||
dev_container_control.update = Save Dev Container
|
dev_container_control.update = Save Dev Container
|
||||||
dev_container_control.create = Create Dev Container
|
dev_container_control.create = Create Dev Container
|
||||||
dev_container_control.stop = Stop Dev Container
|
|
||||||
dev_container_control.start = Start Dev Container
|
|
||||||
dev_container_control.creation_success_for_user = The Dev Container has been created successfully for user '%s'.
|
dev_container_control.creation_success_for_user = The Dev Container has been created successfully for user '%s'.
|
||||||
dev_container_control.creation_failed_for_user = Failed to create the Dev Container for user '%s'.
|
dev_container_control.creation_failed_for_user = Failed to create the Dev Container for user '%s'.
|
||||||
dev_container_control.delete = Delete Dev Container
|
dev_container_control.delete = Delete Dev Container
|
||||||
@@ -3031,7 +3026,6 @@ config_summary = Summary
|
|||||||
config_settings = Settings
|
config_settings = Settings
|
||||||
notices = System Notices
|
notices = System Notices
|
||||||
monitor = Monitoring
|
monitor = Monitoring
|
||||||
devcontainer = devcontainer
|
|
||||||
first_page = First
|
first_page = First
|
||||||
last_page = Last
|
last_page = Last
|
||||||
total = Total: %d
|
total = Total: %d
|
||||||
@@ -3422,7 +3416,6 @@ config.active_code_lives = Active Code Lives
|
|||||||
config.reset_password_code_lives = Recover Account Code Expiry Time
|
config.reset_password_code_lives = Recover Account Code Expiry Time
|
||||||
config.default_keep_email_private = Hide Email Addresses by Default
|
config.default_keep_email_private = Hide Email Addresses by Default
|
||||||
config.default_allow_create_organization = Allow Creation of Organizations by Default
|
config.default_allow_create_organization = Allow Creation of Organizations by Default
|
||||||
config.default_allow_create_devcontainer = Allow Creation of Dev Containers by Default
|
|
||||||
config.enable_timetracking = Enable Time Tracking
|
config.enable_timetracking = Enable Time Tracking
|
||||||
config.default_enable_timetracking = Enable Time Tracking by Default
|
config.default_enable_timetracking = Enable Time Tracking by Default
|
||||||
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
||||||
@@ -3845,24 +3838,6 @@ deletion.success = The secret has been removed.
|
|||||||
deletion.failed = Failed to remove secret.
|
deletion.failed = Failed to remove secret.
|
||||||
management = Secrets Management
|
management = Secrets Management
|
||||||
|
|
||||||
[devcontainer]
|
|
||||||
variables = Variables
|
|
||||||
variables.management = Variables Management
|
|
||||||
variables.creation = Add Variable
|
|
||||||
variables.none = There are no variables yet.
|
|
||||||
variables.deletion = Remove variable
|
|
||||||
variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue?
|
|
||||||
variables.description = 1. As a variable: "$variable name" can be referenced in the variable value and the script specified in devcontainer.json, with the same name priority being: User > Repository > Administration <br>2. As a script: Script management adds variable names to become the initialization script content of the devcontainer.
|
|
||||||
variables.id_not_exist = Variable with ID %d does not exist.
|
|
||||||
variables.edit = Edit Variable
|
|
||||||
variables.deletion.failed = Failed to remove variable.
|
|
||||||
variables.deletion.success = The variable has been removed.
|
|
||||||
variables.creation.failed = Failed to add variable.
|
|
||||||
variables.creation.success = The variable "%s" has been added.
|
|
||||||
variables.update.failed = Failed to edit variable.
|
|
||||||
variables.update.success = The variable has been edited.
|
|
||||||
scripts=Script Management
|
|
||||||
scripts.description=Add variable names to become the initialization script content of the development container, with the same name priority being: User > Repository > Administration
|
|
||||||
[actions]
|
[actions]
|
||||||
actions = Actions
|
actions = Actions
|
||||||
|
|
||||||
|
|||||||
@@ -357,9 +357,7 @@ invalid_log_root_path=日志路径无效: %v
|
|||||||
default_keep_email_private=默认情况下隐藏邮箱地址
|
default_keep_email_private=默认情况下隐藏邮箱地址
|
||||||
default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
|
default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
|
||||||
default_allow_create_organization=默认情况下允许创建组织
|
default_allow_create_organization=默认情况下允许创建组织
|
||||||
default_allow_create_devcontainer=默认情况下允许创建容器
|
|
||||||
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
|
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
|
||||||
default_allow_create_devcontainer_popup=默认情况下, 允许新用户帐户创建容器。
|
|
||||||
default_enable_timetracking=默认情况下启用时间跟踪
|
default_enable_timetracking=默认情况下启用时间跟踪
|
||||||
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
|
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
|
||||||
no_reply_address=隐藏邮件域
|
no_reply_address=隐藏邮件域
|
||||||
@@ -1066,9 +1064,6 @@ visibility.private_tooltip=仅对您已加入的组织的成员可见。
|
|||||||
dev_container = 开发容器
|
dev_container = 开发容器
|
||||||
dev_container_empty = 本仓库没有开发容器配置
|
dev_container_empty = 本仓库没有开发容器配置
|
||||||
dev_container_invalid_config_prompt = 开发容器配置无效:需要上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
|
dev_container_invalid_config_prompt = 开发容器配置无效:需要上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
|
||||||
dev_container_control = 容器管理
|
|
||||||
dev_container_control.stop = 停止开发容器
|
|
||||||
dev_container_control.start = 启动开发容器
|
|
||||||
dev_container_control.update = 保存开发容器
|
dev_container_control.update = 保存开发容器
|
||||||
dev_container_control.create = 创建开发容器
|
dev_container_control.create = 创建开发容器
|
||||||
dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器
|
dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器
|
||||||
@@ -3020,13 +3015,11 @@ config_summary=摘要
|
|||||||
config_settings=设置
|
config_settings=设置
|
||||||
notices=系统提示
|
notices=系统提示
|
||||||
monitor=监控面板
|
monitor=监控面板
|
||||||
devcontainer=开发容器
|
|
||||||
first_page=首页
|
first_page=首页
|
||||||
last_page=末页
|
last_page=末页
|
||||||
total=总计:%d
|
total=总计:%d
|
||||||
settings=管理设置
|
settings=管理设置
|
||||||
|
|
||||||
|
|
||||||
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="%s">博客</a> 了解详情。
|
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="%s">博客</a> 了解详情。
|
||||||
dashboard.statistic=摘要
|
dashboard.statistic=摘要
|
||||||
dashboard.maintenance_operations=运维
|
dashboard.maintenance_operations=运维
|
||||||
@@ -3410,7 +3403,6 @@ config.active_code_lives=激活用户链接有效期
|
|||||||
config.reset_password_code_lives=恢复账户验证码过期时间
|
config.reset_password_code_lives=恢复账户验证码过期时间
|
||||||
config.default_keep_email_private=默认隐藏邮箱地址
|
config.default_keep_email_private=默认隐藏邮箱地址
|
||||||
config.default_allow_create_organization=默认情况下允许创建组织
|
config.default_allow_create_organization=默认情况下允许创建组织
|
||||||
config.default_allow_create_devcontainer=默认情况下允许创建 DevContainer
|
|
||||||
config.enable_timetracking=启用时间跟踪
|
config.enable_timetracking=启用时间跟踪
|
||||||
config.default_enable_timetracking=默认情况下启用时间跟踪
|
config.default_enable_timetracking=默认情况下启用时间跟踪
|
||||||
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
||||||
@@ -3817,7 +3809,7 @@ description=密钥将被传给特定的工作流,其它情况无法读取。
|
|||||||
none=还没有密钥。
|
none=还没有密钥。
|
||||||
|
|
||||||
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
|
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
|
||||||
creation.description=描述
|
creation.description=组织描述
|
||||||
creation.name_placeholder=不区分大小写,仅限字母数字或下划线且不能以 GITEA_ 或 GITHUB_ 开头
|
creation.name_placeholder=不区分大小写,仅限字母数字或下划线且不能以 GITEA_ 或 GITHUB_ 开头
|
||||||
creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略
|
creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略
|
||||||
creation.description_placeholder=输入简短描述(可选)
|
creation.description_placeholder=输入简短描述(可选)
|
||||||
@@ -3833,24 +3825,6 @@ deletion.success=此密钥已删除。
|
|||||||
deletion.failed=删除密钥失败。
|
deletion.failed=删除密钥失败。
|
||||||
management=密钥管理
|
management=密钥管理
|
||||||
|
|
||||||
[devcontainer]
|
|
||||||
variables=变量
|
|
||||||
variables.management=变量管理
|
|
||||||
variables.creation=添加变量
|
|
||||||
variables.none=目前还没有变量。
|
|
||||||
variables.deletion=删除变量
|
|
||||||
variables.deletion.description=删除变量是永久性的,无法撤消。继续吗?
|
|
||||||
variables.description=1.作为变量使用:「$变量名」可以在变量值和devcontainer.json指定的脚本中引用,同名变量优先级:用户>仓库>管理后台。<br>2.作为脚本使用:脚本管理添加变量名成为开发容器的初始化脚本内容。
|
|
||||||
variables.id_not_exist=ID为 %d 的变量不存在。
|
|
||||||
variables.edit=编辑变量
|
|
||||||
variables.deletion.failed=变量删除失败。
|
|
||||||
variables.deletion.success=变量已删除。
|
|
||||||
variables.creation.failed=变量添加失败。
|
|
||||||
variables.creation.success=变量「%s」添加成功。
|
|
||||||
variables.update.failed=变量编辑失败。
|
|
||||||
variables.update.success=变量已编辑。
|
|
||||||
scripts=脚本管理
|
|
||||||
scripts.description=添加变量名成为开发容器的初始化脚本内容,同名脚本优先级:用户>仓库>管理后台。
|
|
||||||
[actions]
|
[actions]
|
||||||
actions=工作流
|
actions=工作流
|
||||||
|
|
||||||
|
|||||||
@@ -971,7 +971,7 @@ owner.settings.cleanuprules.enabled=已啟用
|
|||||||
[secrets]
|
[secrets]
|
||||||
|
|
||||||
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
|
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
|
||||||
creation.description=描述
|
creation.description=組織描述
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,13 +86,6 @@ function install {
|
|||||||
sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION
|
sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION
|
||||||
IMAGE_REGISTRY_USER=devstar.cn/devstar
|
IMAGE_REGISTRY_USER=devstar.cn/devstar
|
||||||
fi
|
fi
|
||||||
if sudo docker pull mengning997/webterminal:latest; then
|
|
||||||
sudo docker tag mengning997/webterminal:latest devstar.cn/devstar/webterminal:latest
|
|
||||||
success "Successfully pulled mengning997/webterminal:latest renamed to devstar.cn/devstar/webterminal:latest"
|
|
||||||
else
|
|
||||||
sudo docker pull devstar.cn/devstar/webterminal:latest
|
|
||||||
success "Successfully pulled devstar.cn/devstar/webterminal:latest"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to start
|
# Function to start
|
||||||
@@ -138,9 +131,6 @@ function stop {
|
|||||||
if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then
|
if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then
|
||||||
sudo docker stop devstar-studio && sudo docker rm -f devstar-studio
|
sudo docker stop devstar-studio && sudo docker rm -f devstar-studio
|
||||||
fi
|
fi
|
||||||
if [ $(docker ps -a --filter "name=^/webterminal-" -q | wc -l) -gt 0 ]; then
|
|
||||||
sudo docker stop $(docker ps -a --filter "name=^/webterminal-" -q) && sudo docker rm -f $(docker ps -a --filter "name=^/webterminal-" -q)
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to logs
|
# Function to logs
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ func Install(ctx *context.Context) {
|
|||||||
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
||||||
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||||
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
||||||
form.DefaultAllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
|
||||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||||
form.NoReplyAddress = setting.Service.NoReplyAddress
|
form.NoReplyAddress = setting.Service.NoReplyAddress
|
||||||
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
||||||
@@ -491,7 +490,6 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
|
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
|
||||||
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
|
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
|
||||||
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
|
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
|
||||||
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").SetValue(strconv.FormatBool(form.DefaultAllowCreateDevcontainer))
|
|
||||||
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
||||||
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
||||||
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
||||||
@@ -650,7 +648,7 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
err = devcontainer_service.RegistWebTerminal(otherCtx)
|
err = devcontainer_service.RegistWebTerminal(otherCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to shutdown the install server! Error: %v", err)
|
ctx.RenderWithErr(ctx.Tr("install.web_terminal_failed", err), tplInstall, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,22 +55,21 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
ctx.Data["ValidateDevContainerConfiguration"] = false
|
ctx.Data["ValidateDevContainerConfiguration"] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["HasDevContainerDockerfile"], ctx.Data["DockerfilePath"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
|
ctx.Data["HasDevContainerDockerfile"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
}
|
}
|
||||||
if ctx.Data["HasDevContainer"] == true {
|
if ctx.Data["HasDevContainer"] == true {
|
||||||
if ctx.Data["HasDevContainerConfiguration"] == true {
|
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
||||||
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
||||||
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
imageName := configurationModel.Image
|
||||||
imageName := configurationModel.Image
|
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
||||||
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
log.Info("%v %v", repo, tag)
|
||||||
log.Info("%v %v", repo, tag)
|
ctx.Data["RepositoryAddress"] = registry
|
||||||
ctx.Data["RepositoryAddress"] = registry
|
ctx.Data["RepositoryUsername"] = namespace
|
||||||
ctx.Data["RepositoryUsername"] = namespace
|
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||||
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
|
||||||
}
|
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
// 获取WebSSH服务端口
|
// 获取WebSSH服务端口
|
||||||
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
@@ -112,6 +111,7 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
|
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
@@ -136,6 +136,7 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
|
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -144,6 +145,7 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 携带数据渲染页面,返回
|
// 3. 携带数据渲染页面,返回
|
||||||
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
||||||
ctx.Data["PageIsDevContainer"] = true
|
ctx.Data["PageIsDevContainer"] = true
|
||||||
@@ -181,31 +183,25 @@ func CreateDevContainerConfiguration(ctx *context.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if hasDevContainerConfiguration {
|
if hasDevContainerConfiguration {
|
||||||
ctx.Flash.Error("Already exist", true)
|
ctx.Flash.Error("Already exist", true)
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isAdmin, err := devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
|
isAdmin, err := devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isAdmin {
|
if !isAdmin {
|
||||||
ctx.Flash.Error("permisson denied", true)
|
ctx.Flash.Error("permisson denied", true)
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = devcontainer_service.CreateDevcontainerConfiguration(ctx.Repo.Repository, ctx.Doer)
|
err = devcontainer_service.CreateDevcontainerConfiguration(ctx.Repo.Repository, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
||||||
@@ -260,7 +256,7 @@ func RestartDevContainer(ctx *context.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"status": "6"})
|
ctx.JSON(http.StatusOK, "")
|
||||||
}
|
}
|
||||||
func StopDevContainer(ctx *context.Context) {
|
func StopDevContainer(ctx *context.Context) {
|
||||||
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
@@ -277,7 +273,7 @@ func StopDevContainer(ctx *context.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"status": "7"})
|
ctx.JSON(http.StatusOK, "")
|
||||||
}
|
}
|
||||||
func UpdateDevContainer(ctx *context.Context) {
|
func UpdateDevContainer(ctx *context.Context) {
|
||||||
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
@@ -298,7 +294,7 @@ func UpdateDevContainer(ctx *context.Context) {
|
|||||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo, &updateInfo)
|
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo.Repository, &updateInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -316,43 +312,18 @@ func GetTerminalCommand(ctx *context.Context) {
|
|||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
status = "error"
|
status = "error"
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status, "workdir": "/workspace/" + ctx.Repo.Repository.Name})
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDevContainerOutput(ctx *context.Context) {
|
func GetDevContainerOutput(ctx *context.Context) {
|
||||||
// 设置 CORS 响应头
|
// 设置 CORS 响应头
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
query := ctx.Req.URL.Query()
|
output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||||
output, err := devcontainer_service.GetDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"output": output})
|
ctx.JSON(http.StatusOK, output)
|
||||||
}
|
|
||||||
func SaveDevContainerOutput(ctx *context.Context) {
|
|
||||||
// 设置 CORS 响应头
|
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
|
||||||
// 处理 OPTIONS 预检请求
|
|
||||||
if ctx.Req.Method == "OPTIONS" {
|
|
||||||
ctx.JSON(http.StatusOK, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := ctx.Req.URL.Query()
|
|
||||||
|
|
||||||
// 从请求体中读取输出内容
|
|
||||||
body, err := io.ReadAll(ctx.Req.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read request body: %v", err)
|
|
||||||
ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = devcontainer_service.SaveDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository, string(body))
|
|
||||||
if err != nil {
|
|
||||||
log.Info(err.Error())
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, "")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TplVscodeHome 显示 DevStar Home 页面 templates/vscode-home.tmpl
|
// TplDevstarHome 显示 DevStar Home 页面 templates/vscode-home.tmpl
|
||||||
TplVscodeHome templates.TplName = "repo/devcontainer/vscode-home"
|
TplDevstarHome templates.TplName = "repo/devcontainer/vscode-home"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VscodeHome 渲染适配于 VSCode 插件的 DevStar Home 页面
|
// DevstarHome 渲染适配于 VSCode 插件的 DevStar Home 页面
|
||||||
func VscodeHome(ctx *gitea_web_context.Context) {
|
func DevstarHome(ctx *gitea_web_context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("home")
|
ctx.Data["Title"] = ctx.Tr("home")
|
||||||
ctx.Resp.Header().Del("X-Frame-Options")
|
ctx.Resp.Header().Del("X-Frame-Options")
|
||||||
//ctx.Resp.Header().Set("Content-Security-Policy", "frame-ancestors *")
|
//ctx.Resp.Header().Set("Content-Security-Policy", "frame-ancestors *")
|
||||||
ctx.HTML(http.StatusOK, TplVscodeHome)
|
ctx.HTML(http.StatusOK, TplDevstarHome)
|
||||||
}
|
}
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package devcontainer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/templates"
|
|
||||||
"code.gitea.io/gitea/modules/web"
|
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
|
||||||
"code.gitea.io/gitea/services/context"
|
|
||||||
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
|
|
||||||
"code.gitea.io/gitea/services/forms"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tplRepoVariables templates.TplName = "repo/settings/devcontainer"
|
|
||||||
tplOrgVariables templates.TplName = "org/settings/devcontainer"
|
|
||||||
tplUserVariables templates.TplName = "user/settings/devcontainer"
|
|
||||||
tplAdminVariables templates.TplName = "admin/devcontainer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type variablesCtx struct {
|
|
||||||
OwnerID int64
|
|
||||||
RepoID int64
|
|
||||||
IsRepo bool
|
|
||||||
IsOrg bool
|
|
||||||
IsUser bool
|
|
||||||
IsGlobal bool
|
|
||||||
VariablesTemplate templates.TplName
|
|
||||||
RedirectLink string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
|
||||||
if ctx.Data["PageIsRepoSettings"] == true {
|
|
||||||
return &variablesCtx{
|
|
||||||
OwnerID: 0,
|
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
|
||||||
IsRepo: true,
|
|
||||||
VariablesTemplate: tplRepoVariables,
|
|
||||||
RedirectLink: ctx.Repo.RepoLink + "/settings/devcontainer/variables",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Data["PageIsOrgSettings"] == true {
|
|
||||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
|
||||||
ctx.ServerError("RenderUserOrgHeader", err)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return &variablesCtx{
|
|
||||||
OwnerID: ctx.ContextUser.ID,
|
|
||||||
RepoID: 0,
|
|
||||||
IsOrg: true,
|
|
||||||
VariablesTemplate: tplOrgVariables,
|
|
||||||
RedirectLink: ctx.Org.OrgLink + "/settings/devcontainer/variables",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Data["PageIsUserSettings"] == true {
|
|
||||||
return &variablesCtx{
|
|
||||||
OwnerID: ctx.Doer.ID,
|
|
||||||
RepoID: 0,
|
|
||||||
IsUser: true,
|
|
||||||
VariablesTemplate: tplUserVariables,
|
|
||||||
RedirectLink: setting.AppSubURL + "/user/settings/devcontainer/variables",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Data["PageIsAdmin"] == true {
|
|
||||||
return &variablesCtx{
|
|
||||||
OwnerID: 0,
|
|
||||||
RepoID: 0,
|
|
||||||
IsGlobal: true,
|
|
||||||
VariablesTemplate: tplAdminVariables,
|
|
||||||
RedirectLink: setting.AppSubURL + "/-/admin/devcontainer/variables",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("unable to set Variables context")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Variables(ctx *context.Context) {
|
|
||||||
ctx.Data["Title"] = ctx.Tr("devcontainer.variables")
|
|
||||||
ctx.Data["PageType"] = "variables"
|
|
||||||
ctx.Data["PageIsSharedSettingsDevcontainerVariables"] = true
|
|
||||||
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
variables, err := db.Find[devcontainer_model.DevcontainerVariable](ctx, devcontainer_model.FindVariablesOpts{
|
|
||||||
OwnerID: vCtx.OwnerID,
|
|
||||||
RepoID: vCtx.RepoID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("FindVariables", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags []string
|
|
||||||
|
|
||||||
// 使用JOIN查询,关联DevcontainerScript表和devcontainer_variable表
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Select("variable_name").
|
|
||||||
Table("devcontainer_script").
|
|
||||||
Where("user_id = ? AND repo_id = ?", vCtx.OwnerID, vCtx.RepoID).
|
|
||||||
Find(&tags)
|
|
||||||
|
|
||||||
// 将tags转换为JSON格式的字符串
|
|
||||||
tagsJSON, err := json.Marshal(tags)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("Marshal tags", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 确保tagsJSON不为null
|
|
||||||
tagsJSONStr := string(tagsJSON)
|
|
||||||
if tagsJSONStr == "null" {
|
|
||||||
tagsJSONStr = "[]"
|
|
||||||
}
|
|
||||||
// 创建一个新的请求
|
|
||||||
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
|
|
||||||
} else {
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
|
|
||||||
} else {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
|
|
||||||
} else {
|
|
||||||
var devstarVariables []*devcontainer_model.DevcontainerVariable
|
|
||||||
err = json.Unmarshal(body, &devstarVariables)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
|
|
||||||
} else {
|
|
||||||
// 创建一个本地变量名称的映射,用于快速查找
|
|
||||||
localVariableNames := make(map[string]bool)
|
|
||||||
for _, variable := range variables {
|
|
||||||
localVariableNames[variable.Name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选出不与本地变量同名的devstar变量
|
|
||||||
var filteredDevstarVariables []*devcontainer_model.DevcontainerVariable
|
|
||||||
for _, devstarVar := range devstarVariables {
|
|
||||||
if !localVariableNames[devstarVar.Name] {
|
|
||||||
filteredDevstarVariables = append(filteredDevstarVariables, devstarVar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["DevstarVariables"] = filteredDevstarVariables
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Data["Variables"] = variables
|
|
||||||
ctx.Data["Tags"] = tagsJSONStr
|
|
||||||
ctx.Data["DescriptionMaxLength"] = devcontainer_model.VariableDescriptionMaxLength
|
|
||||||
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
|
|
||||||
}
|
|
||||||
func GetExportVariables(ctx *context.Context) {
|
|
||||||
globalVariables, err := db.Find[devcontainer_model.DevcontainerVariable](ctx, devcontainer_model.FindVariablesOpts{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("Get Global Variables", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 筛选出键以"DEVSTAR_"开头的脚本
|
|
||||||
var devstarVariables []devcontainer_model.DevcontainerVariable
|
|
||||||
for _, value := range globalVariables {
|
|
||||||
if strings.HasPrefix(value.Name, "DEVSTAR_") {
|
|
||||||
devstarVariables = append(devstarVariables, *value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, devstarVariables)
|
|
||||||
}
|
|
||||||
func VariableCreate(ctx *context.Context) {
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.HasError() { // form binding validation error
|
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
|
||||||
|
|
||||||
v, err := devcontainer_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data, form.Description)
|
|
||||||
if err != nil {
|
|
||||||
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(vCtx.RedirectLink)
|
|
||||||
}
|
|
||||||
func ScriptCreate(ctx *context.Context) {
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.HasError() { // form binding validation error
|
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
query := ctx.Req.URL.Query()
|
|
||||||
var script *devcontainer_model.DevcontainerScript
|
|
||||||
// 首先检查变量是否在DevcontainerVariable表中存在
|
|
||||||
exists, err := db.GetEngine(ctx).
|
|
||||||
Table("devcontainer_variable").
|
|
||||||
Where("(owner_id = 0 AND repo_id = 0) OR (owner_id = ? AND repo_id = 0) OR (owner_id = 0 AND repo_id = ?)", vCtx.OwnerID, vCtx.RepoID).
|
|
||||||
And("name = ?", strings.ToUpper(query.Get("name"))).
|
|
||||||
Exist()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Check variable existence: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
// 创建一个新的请求来获取devstar变量
|
|
||||||
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create request for devstar variables: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to fetch devstar variables: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read devstar variables response: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var devstarVariables []*devcontainer_model.DevcontainerVariable
|
|
||||||
err = json.Unmarshal(body, &devstarVariables)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to unmarshal devstar variables: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找是否有匹配的devstar变量
|
|
||||||
foundInDevstar := false
|
|
||||||
searchName := strings.ToUpper(query.Get("name"))
|
|
||||||
for _, devstarVar := range devstarVariables {
|
|
||||||
if devstarVar.Name == searchName {
|
|
||||||
foundInDevstar = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundInDevstar {
|
|
||||||
log.Error("Variable %s does not exist", query.Get("name"))
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// 创建devcontainer_script记录
|
|
||||||
script = &devcontainer_model.DevcontainerScript{
|
|
||||||
UserId: vCtx.OwnerID,
|
|
||||||
RepoId: vCtx.RepoID,
|
|
||||||
VariableName: strings.ToUpper(query.Get("name")),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.GetEngine(ctx).Insert(script)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("CreateScript: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func VariableUpdate(ctx *context.Context) {
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.HasError() { // form binding validation error
|
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id := ctx.PathParamInt64("variable_id")
|
|
||||||
|
|
||||||
variable := findActionsVariable(ctx, id, vCtx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
|
||||||
variable.Name = form.Name
|
|
||||||
variable.Data = form.Data
|
|
||||||
variable.Description = form.Description
|
|
||||||
|
|
||||||
if ok, err := devcontainer_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
|
|
||||||
log.Error("UpdateVariable: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
|
|
||||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *devcontainer_model.DevcontainerVariable {
|
|
||||||
opts := devcontainer_model.FindVariablesOpts{
|
|
||||||
IDs: []int64{id},
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case vCtx.IsRepo:
|
|
||||||
opts.RepoID = vCtx.RepoID
|
|
||||||
if opts.RepoID == 0 {
|
|
||||||
panic("RepoID is 0")
|
|
||||||
}
|
|
||||||
case vCtx.IsOrg, vCtx.IsUser:
|
|
||||||
opts.OwnerID = vCtx.OwnerID
|
|
||||||
if opts.OwnerID == 0 {
|
|
||||||
panic("OwnerID is 0")
|
|
||||||
}
|
|
||||||
case vCtx.IsGlobal:
|
|
||||||
// do nothing
|
|
||||||
default:
|
|
||||||
panic("invalid actions variable")
|
|
||||||
}
|
|
||||||
got, err := devcontainer_model.FindVariables(ctx, opts)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("FindVariables", err)
|
|
||||||
return nil
|
|
||||||
} else if len(got) == 0 {
|
|
||||||
ctx.NotFound(nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return got[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func VariableDelete(ctx *context.Context) {
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id := ctx.PathParamInt64("variable_id")
|
|
||||||
|
|
||||||
variable := findActionsVariable(ctx, id, vCtx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := devcontainer_service.DeleteVariableByID(ctx, variable.ID); err != nil {
|
|
||||||
log.Error("Delete variable [%d] failed: %v", id, err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 删除相应的script记录,根据repoId、userId和name
|
|
||||||
script := &devcontainer_model.DevcontainerScript{
|
|
||||||
UserId: vCtx.OwnerID,
|
|
||||||
RepoId: vCtx.RepoID,
|
|
||||||
VariableName: variable.Name,
|
|
||||||
}
|
|
||||||
_, err = db.GetEngine(ctx).Delete(script)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Delete script for variable [%d] failed: %v", id, err)
|
|
||||||
// 注意:这里我们记录错误但不中断变量删除过程
|
|
||||||
}
|
|
||||||
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
|
||||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
|
||||||
}
|
|
||||||
func ScriptDelete(ctx *context.Context) {
|
|
||||||
vCtx, err := getVariablesCtx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("getVariablesCtx", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.HasError() { // form binding validation error
|
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
query := ctx.Req.URL.Query()
|
|
||||||
// 删除devcontainer_script记录
|
|
||||||
script := &devcontainer_model.DevcontainerScript{
|
|
||||||
UserId: vCtx.OwnerID,
|
|
||||||
RepoId: vCtx.RepoID,
|
|
||||||
VariableName: query.Get("name"),
|
|
||||||
}
|
|
||||||
_, err = db.GetEngine(ctx).Delete(script)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("DeleteScript: %v", err)
|
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/issues"
|
"code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
@@ -26,7 +25,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
|
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
)
|
)
|
||||||
@@ -413,23 +411,6 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info("File deleted: %s", treePath)
|
|
||||||
if treePath == `.devcontainer/devcontainer.json` {
|
|
||||||
var userIds []int64
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Table("devcontainer").
|
|
||||||
Select("user_id").
|
|
||||||
Where("repo_id = ?", ctx.Repo.Repository.ID).
|
|
||||||
Find(&userIds)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetEngine", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, userId := range userIds {
|
|
||||||
devcontainer_service.DeleteDevContainer(ctx, userId, ctx.Repo.Repository.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import (
|
|||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
|
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,13 +208,6 @@ func KeysPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 将公钥添加到所有打开的容器中
|
|
||||||
log.Info("将公钥添加到所有打开的容器中")
|
|
||||||
err = devcontainer_service.AddPublicKeyToAllRunningDevContainer(ctx, ctx.Doer.ID, content)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("AddPublicKey To Devcontainer", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||||
case "verify_ssh":
|
case "verify_ssh":
|
||||||
|
|||||||
@@ -457,20 +457,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addSettingsDevcontainerVariablesRoutes := func() {
|
|
||||||
m.Group("/variables", func() {
|
|
||||||
m.Get("", devcontainer_web.Variables)
|
|
||||||
m.Post("/new", web.Bind(forms.EditVariableForm{}), devcontainer_web.VariableCreate)
|
|
||||||
m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), devcontainer_web.VariableUpdate)
|
|
||||||
m.Post("/{variable_id}/delete", devcontainer_web.VariableDelete)
|
|
||||||
m.Group("/script", func() {
|
|
||||||
m.Get("/new", devcontainer_web.ScriptCreate)
|
|
||||||
m.Get("/delete", devcontainer_web.ScriptDelete)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
addSettingsSecretsRoutes := func() {
|
addSettingsSecretsRoutes := func() {
|
||||||
m.Group("/secrets", func() {
|
m.Group("/secrets", func() {
|
||||||
m.Get("", repo_setting.Secrets)
|
m.Get("", repo_setting.Secrets)
|
||||||
@@ -503,7 +489,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
||||||
|
|
||||||
m.Get("/", Home)
|
m.Get("/", Home)
|
||||||
m.Get("/variables/export", devcontainer_web.GetExportVariables)
|
|
||||||
m.Get("/sitemap.xml", sitemapEnabled, optExploreSignIn, HomeSitemap)
|
m.Get("/sitemap.xml", sitemapEnabled, optExploreSignIn, HomeSitemap)
|
||||||
m.Group("/.well-known", func() {
|
m.Group("/.well-known", func() {
|
||||||
m.Get("/openid-configuration", auth.OIDCWellKnown)
|
m.Get("/openid-configuration", auth.OIDCWellKnown)
|
||||||
@@ -699,9 +684,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
addSettingsSecretsRoutes()
|
addSettingsSecretsRoutes()
|
||||||
addSettingsVariablesRoutes()
|
addSettingsVariablesRoutes()
|
||||||
}, actions.MustEnableActions)
|
}, actions.MustEnableActions)
|
||||||
m.Group("/devcontainer", func() {
|
|
||||||
addSettingsDevcontainerVariablesRoutes()
|
|
||||||
})
|
|
||||||
|
|
||||||
m.Get("/organization", user_setting.Organization)
|
m.Get("/organization", user_setting.Organization)
|
||||||
m.Get("/repos", user_setting.Repos)
|
m.Get("/repos", user_setting.Repos)
|
||||||
@@ -780,9 +762,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
})
|
})
|
||||||
m.Get("/diagnosis", admin.MonitorDiagnosis)
|
m.Get("/diagnosis", admin.MonitorDiagnosis)
|
||||||
})
|
})
|
||||||
m.Group("/devcontainer", func() {
|
|
||||||
addSettingsDevcontainerVariablesRoutes()
|
|
||||||
})
|
|
||||||
|
|
||||||
m.Group("/users", func() {
|
m.Group("/users", func() {
|
||||||
m.Get("", admin.Users)
|
m.Get("", admin.Users)
|
||||||
@@ -1010,9 +989,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
addSettingsVariablesRoutes()
|
addSettingsVariablesRoutes()
|
||||||
}, actions.MustEnableActions)
|
}, actions.MustEnableActions)
|
||||||
|
|
||||||
m.Group("/devcontainer", func() {
|
|
||||||
addSettingsDevcontainerVariablesRoutes()
|
|
||||||
})
|
|
||||||
m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
|
m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
|
||||||
m.Post("/delete", org.SettingsDeleteOrgPost)
|
m.Post("/delete", org.SettingsDeleteOrgPost)
|
||||||
|
|
||||||
@@ -1205,10 +1181,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
addSettingsSecretsRoutes()
|
addSettingsSecretsRoutes()
|
||||||
addSettingsVariablesRoutes()
|
addSettingsVariablesRoutes()
|
||||||
}, actions.MustEnableActions)
|
}, actions.MustEnableActions)
|
||||||
|
|
||||||
m.Group("/devcontainer", func() {
|
|
||||||
addSettingsDevcontainerVariablesRoutes()
|
|
||||||
})
|
|
||||||
// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
|
// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
|
||||||
m.Group("/migrate", func() {
|
m.Group("/migrate", func() {
|
||||||
m.Post("/retry", repo.MigrateRetryPost)
|
m.Post("/retry", repo.MigrateRetryPost)
|
||||||
@@ -1434,14 +1406,12 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Get("/status", devcontainer_web.GetDevContainerStatus)
|
m.Get("/status", devcontainer_web.GetDevContainerStatus)
|
||||||
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
||||||
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
||||||
m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
|
|
||||||
},
|
},
|
||||||
// 解析仓库信息
|
// 解析仓库信息
|
||||||
// 具有code读取权限
|
// 具有code读取权限
|
||||||
context.RepoAssignment, reqUnitCodeReader,
|
context.RepoAssignment, reqUnitCodeReader,
|
||||||
)
|
)
|
||||||
m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
|
m.Get("/devstar-home", devcontainer_web.DevstarHome)
|
||||||
m.Get("/vscode-home", devcontainer_web.VscodeHome)
|
|
||||||
m.Group("/api/devcontainer", func() {
|
m.Group("/api/devcontainer", func() {
|
||||||
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
||||||
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"regexp"
|
"net"
|
||||||
"strconv"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,12 +17,10 @@ import (
|
|||||||
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
|
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
|
||||||
"code.gitea.io/gitea/models/repo"
|
"code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/user"
|
"code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/docker"
|
|
||||||
docker_module "code.gitea.io/gitea/modules/docker"
|
docker_module "code.gitea.io/gitea/modules/docker"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@@ -68,21 +67,21 @@ func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repos
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, string, error) {
|
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
|
||||||
_, err := FileExists(".devcontainer/devcontainer.json", repo)
|
_, err := FileExists(".devcontainer/devcontainer.json", repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
return false, "", nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, "", err
|
return false, err
|
||||||
}
|
}
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, err
|
||||||
}
|
}
|
||||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, err
|
||||||
}
|
}
|
||||||
// 执行验证
|
// 执行验证
|
||||||
if errs := configurationModel.Validate(); len(errs) > 0 {
|
if errs := configurationModel.Validate(); len(errs) > 0 {
|
||||||
@@ -90,48 +89,45 @@ func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Reposito
|
|||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
fmt.Printf(" - %s\n", err.Error())
|
fmt.Printf(" - %s\n", err.Error())
|
||||||
}
|
}
|
||||||
return false, "", fmt.Errorf("配置格式错误")
|
return false, fmt.Errorf("配置格式错误")
|
||||||
} else {
|
} else {
|
||||||
log.Info("%v", configurationModel)
|
log.Info("%v", configurationModel)
|
||||||
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
||||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
return false, nil
|
||||||
if err != nil {
|
|
||||||
if git.IsErrNotExist(err) {
|
|
||||||
return false, "", nil
|
|
||||||
}
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
return true, ".devcontainer/Dockerfile", nil
|
|
||||||
}
|
}
|
||||||
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
return false, nil
|
||||||
if err != nil {
|
|
||||||
if git.IsErrNotExist(err) {
|
|
||||||
return false, "", nil
|
|
||||||
}
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
return true, ".devcontainer/Dockerfile", nil
|
|
||||||
}
|
}
|
||||||
return false, "", err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, ".devcontainer/" + configurationModel.Build.Dockerfile, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
|
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
|
||||||
|
jsonString := `{
|
||||||
jsonContent, err := templates.AssetFS().ReadFile("repo/devcontainer/default_devcontainer.json")
|
"name":"template",
|
||||||
if err != nil {
|
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
|
||||||
return err
|
"forwardPorts": ["8080"],
|
||||||
}
|
"containerEnv": {
|
||||||
_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"initializeCommand": "echo \"init\";",
|
||||||
|
"postCreateCommand": [
|
||||||
|
"echo \"created\"",
|
||||||
|
"echo \"test\""
|
||||||
|
],
|
||||||
|
"runArgs": [
|
||||||
|
"-p 8888"
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
|
||||||
Files: []*files_service.ChangeRepoFile{
|
Files: []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
Operation: "create",
|
Operation: "create",
|
||||||
TreePath: ".devcontainer/devcontainer.json",
|
TreePath: ".devcontainer/devcontainer.json",
|
||||||
ContentReader: bytes.NewReader([]byte(jsonContent)),
|
ContentReader: bytes.NewReader([]byte(jsonString)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OldBranch: "main",
|
OldBranch: "main",
|
||||||
@@ -447,7 +443,7 @@ func StopDevContainer(ctx context.Context, userID, repoID int64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_context.Repository, updateInfo *UpdateInfo) error {
|
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error {
|
||||||
dbEngine := db.GetEngine(ctx)
|
dbEngine := db.GetEngine(ctx)
|
||||||
var devContainerInfo devcontainer_models.Devcontainer
|
var devContainerInfo devcontainer_models.Devcontainer
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
@@ -457,24 +453,25 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_contex
|
|||||||
_, err = dbEngine.
|
_, err = dbEngine.
|
||||||
Table("devcontainer").
|
Table("devcontainer").
|
||||||
Select("*").
|
Select("*").
|
||||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID).
|
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||||
Get(&devContainerInfo)
|
Get(&devContainerInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = dbEngine.Table("devcontainer").
|
_, err = dbEngine.Table("devcontainer").
|
||||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
|
||||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
|
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
otherCtx := context.Background()
|
otherCtx := context.Background()
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
} else {
|
} else {
|
||||||
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
||||||
_, err = dbEngine.Table("devcontainer").
|
_, err = dbEngine.Table("devcontainer").
|
||||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
|
||||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -545,72 +542,67 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
//正在创建容器,创建容器成功,则状态转移
|
//正在创建容器,创建容器成功,则状态转移
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
exist, _, err := ContainerExists(ctx, devContainerInfo.Name)
|
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if !exist {
|
if status == "running" {
|
||||||
_, err = dbEngine.Table("devcontainer_output").
|
//添加脚本文件
|
||||||
Select("command").
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
|
|
||||||
Get(&cmd)
|
} else {
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
var scriptContent []byte
|
||||||
}
|
_, err = os.Stat("webTerminal.sh")
|
||||||
} else {
|
if os.IsNotExist(err) {
|
||||||
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
_, err = os.Stat("/app/gitea/webTerminal.sh")
|
||||||
if err != nil {
|
if os.IsNotExist(err) {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
} else {
|
||||||
if status == "created" {
|
scriptContent, err = os.ReadFile("/app/gitea/webTerminal.sh")
|
||||||
//添加脚本文件
|
if err != nil {
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
userNum, err := strconv.ParseInt(userID, 10, 64)
|
scriptContent, err = os.ReadFile("webTerminal.sh")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
var scriptContent string
|
|
||||||
scriptContent, err = GetCommandContent(ctx, userNum, repo)
|
|
||||||
log.Info("command: %s", scriptContent)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
// 创建 tar 归档文件
|
|
||||||
var buf bytes.Buffer
|
|
||||||
tw := tar.NewWriter(&buf)
|
|
||||||
defer tw.Close()
|
|
||||||
// 添加文件到 tar 归档
|
|
||||||
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
|
|
||||||
// 创建 Docker 客户端
|
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
// 获取容器 ID
|
|
||||||
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
|
|
||||||
if err != nil {
|
|
||||||
log.Info("%v", err)
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
realTimeStatus = 3
|
// 创建 tar 归档文件
|
||||||
|
var buf bytes.Buffer
|
||||||
|
tw := tar.NewWriter(&buf)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// 添加文件到 tar 归档
|
||||||
|
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
|
||||||
|
// 创建 Docker 客户端
|
||||||
|
cli, err := docker_module.CreateDockerClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
// 获取容器 ID
|
||||||
|
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Info("%v", err)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
realTimeStatus = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
@@ -618,6 +610,7 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir)
|
status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@@ -639,27 +632,6 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
postAttachCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostAttachCommand), "\n"))
|
|
||||||
if _, ok := configurationModel.PostAttachCommand.(map[string]interface{}); ok {
|
|
||||||
// 是 map[string]interface{} 类型
|
|
||||||
cmdObj := configurationModel.PostAttachCommand.(map[string]interface{})
|
|
||||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
|
||||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
postAttachCommand += "\n" + fileCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmd += postAttachCommand
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -682,59 +654,67 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
}
|
}
|
||||||
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
||||||
}
|
}
|
||||||
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
|
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
|
||||||
var devContainerOutput string
|
var devContainerOutput []devcontainer_models.DevcontainerOutput
|
||||||
dbEngine := db.GetEngine(ctx)
|
dbEngine := db.GetEngine(ctx)
|
||||||
|
resp := OutputResponse{}
|
||||||
|
var status string
|
||||||
|
var containerName string
|
||||||
|
_, err := dbEngine.
|
||||||
|
Table("devcontainer").
|
||||||
|
Select("devcontainer_status, name").
|
||||||
|
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||||
|
Get(&status, &containerName)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
_, err := dbEngine.Table("devcontainer_output").
|
err = dbEngine.Table("devcontainer_output").
|
||||||
Select("output").
|
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
Find(&devContainerOutput)
|
||||||
Get(&devContainerOutput)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return resp, err
|
||||||
}
|
}
|
||||||
if devContainerOutput != "" {
|
|
||||||
_, err = dbEngine.Table("devcontainer_output").
|
if len(devContainerOutput) > 0 {
|
||||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
|
||||||
Update(map[string]interface{}{
|
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
|
||||||
"output": "",
|
resp.CurrentJob.Detail = status
|
||||||
|
if status == "4" {
|
||||||
|
// 获取WebSSH服务端口
|
||||||
|
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
|
||||||
|
if err == nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
// 解析URL
|
||||||
|
u, err := url.Parse(webTerminalURL)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
// 分离主机和端口
|
||||||
|
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
|
||||||
|
resp.CurrentJob.IP = terminalHost
|
||||||
|
resp.CurrentJob.Port = terminalPort
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range devContainerOutput {
|
||||||
|
logLines := []ViewStepLogLine{}
|
||||||
|
logLines = append(logLines, ViewStepLogLine{
|
||||||
|
Index: 1,
|
||||||
|
Message: item.Output,
|
||||||
})
|
})
|
||||||
if err != nil {
|
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
|
||||||
return "", err
|
Summary: item.Command,
|
||||||
|
Status: item.Status,
|
||||||
|
Logs: logLines,
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return resp, nil
|
||||||
return devContainerOutput, nil
|
|
||||||
}
|
|
||||||
func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error {
|
|
||||||
var devContainerOutput string
|
|
||||||
var finalOutput string
|
|
||||||
dbEngine := db.GetEngine(ctx)
|
|
||||||
|
|
||||||
// 从数据库中获取现有的输出内容
|
|
||||||
_, err := dbEngine.Table("devcontainer_output").
|
|
||||||
Select("output").
|
|
||||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
|
||||||
Get(&devContainerOutput)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n")
|
|
||||||
if newoutput == "\b \b" {
|
|
||||||
finalOutput = devContainerOutput[:len(devContainerOutput)-1]
|
|
||||||
} else {
|
|
||||||
finalOutput = devContainerOutput + newoutput
|
|
||||||
}
|
|
||||||
_, err = dbEngine.Table("devcontainer_output").
|
|
||||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
|
||||||
Update(map[string]interface{}{
|
|
||||||
"output": finalOutput + "\r\n",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
@@ -911,184 +891,3 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gite
|
|||||||
"&devstar_username=" + repo.Repository.OwnerName +
|
"&devstar_username=" + repo.Repository.OwnerName +
|
||||||
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
|
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
|
||||||
}
|
}
|
||||||
func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository) (string, error) {
|
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
onCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.OnCreateCommand), "\n"))
|
|
||||||
if _, ok := configurationModel.OnCreateCommand.(map[string]interface{}); ok {
|
|
||||||
// 是 map[string]interface{} 类型
|
|
||||||
cmdObj := configurationModel.OnCreateCommand.(map[string]interface{})
|
|
||||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
|
||||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
onCreateCommand += "\n" + fileCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.UpdateContentCommand), "\n"))
|
|
||||||
if _, ok := configurationModel.UpdateContentCommand.(map[string]interface{}); ok {
|
|
||||||
// 是 map[string]interface{} 类型
|
|
||||||
cmdObj := configurationModel.UpdateContentCommand.(map[string]interface{})
|
|
||||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
|
||||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
updateCommand += "\n" + fileCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostCreateCommand), "\n"))
|
|
||||||
if _, ok := configurationModel.PostCreateCommand.(map[string]interface{}); ok {
|
|
||||||
// 是 map[string]interface{} 类型
|
|
||||||
cmdObj := configurationModel.PostCreateCommand.(map[string]interface{})
|
|
||||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
|
||||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
postCreateCommand += "\n" + fileCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postStartCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostStartCommand), "\n"))
|
|
||||||
if _, ok := configurationModel.PostStartCommand.(map[string]interface{}); ok {
|
|
||||||
// 是 map[string]interface{} 类型
|
|
||||||
cmdObj := configurationModel.PostStartCommand.(map[string]interface{})
|
|
||||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
|
||||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
postStartCommand += "\n" + fileCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var script []string
|
|
||||||
scripts, err := devcontainer_models.GetScript(ctx, userId, repo.ID)
|
|
||||||
for _, v := range scripts {
|
|
||||||
script = append(script, v)
|
|
||||||
}
|
|
||||||
scriptCommand := strings.TrimSpace(strings.Join(script, "\n"))
|
|
||||||
userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n"
|
|
||||||
assetFS := templates.AssetFS()
|
|
||||||
Content_tmpl, err := assetFS.ReadFile("repo/devcontainer/devcontainer_tmpl.sh")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
Content_start, err := assetFS.ReadFile("repo/devcontainer/devcontainer_start.sh")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
Content_restart, err := assetFS.ReadFile("repo/devcontainer/devcontainer_restart.sh")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
final_command := string(Content_tmpl)
|
|
||||||
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("START") + `\}|` + `\$` + regexp.QuoteMeta("START") + `\b`)
|
|
||||||
escapedContentStart := strings.ReplaceAll(string(Content_start), `$`, `$$`)
|
|
||||||
escapedUserCommand := strings.ReplaceAll(userCommand, `$`, `$$`)
|
|
||||||
final_command = re1.ReplaceAllString(final_command, escapedContentStart+"\n"+escapedUserCommand)
|
|
||||||
|
|
||||||
re1 = regexp.MustCompile(`\$RESTART\b`)
|
|
||||||
escapedContentRestart := strings.ReplaceAll(string(Content_restart), `$`, `$$`)
|
|
||||||
escapedPostStartCommand := strings.ReplaceAll(postStartCommand, `$`, `$$`)
|
|
||||||
final_command = re1.ReplaceAllString(final_command, escapedContentRestart+"\n"+escapedPostStartCommand)
|
|
||||||
return parseCommand(ctx, final_command, userId, repo)
|
|
||||||
}
|
|
||||||
func AddPublicKeyToAllRunningDevContainer(ctx context.Context, userId int64, publicKey string) error {
|
|
||||||
// 加载配置文件
|
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
cli, err := docker.CreateDockerClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
var devcontainerList []devcontainer_models.Devcontainer
|
|
||||||
// 查询所有打开的容器
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Table("devcontainer").
|
|
||||||
Where("user_id = ? AND devcontainer_status = ?", userId, 4).
|
|
||||||
Find(&devcontainerList)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(devcontainerList) > 0 {
|
|
||||||
// 将公钥写入这些打开的容器中
|
|
||||||
for _, repoDevContainer := range devcontainerList {
|
|
||||||
containerID, err := docker.GetContainerID(cli, repoDevContainer.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("container id: %s, name: %s", containerID, repoDevContainer.Name)
|
|
||||||
// 检查容器状态
|
|
||||||
containerStatus, err := docker.GetContainerStatus(cli, containerID)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerStatus == "running" {
|
|
||||||
// 只为处于运行状态的容器添加公钥
|
|
||||||
_, err = docker.ExecCommandInContainer(ctx, cli, repoDevContainer.Name, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", publicKey))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unknown agent")
|
|
||||||
|
|
||||||
}
|
|
||||||
func parseCommand(ctx context.Context, command string, userId int64, repo *repo.Repository) (string, error) {
|
|
||||||
variables, err := devcontainer_models.GetVariables(ctx, userId, repo.ID)
|
|
||||||
|
|
||||||
var variablesName []string
|
|
||||||
variablesCircle := checkEachVariable(variables)
|
|
||||||
for key := range variables {
|
|
||||||
if !variablesCircle[key] {
|
|
||||||
variablesName = append(variablesName, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ContainsAnySubstring(command, variablesName) {
|
|
||||||
for key, value := range variables {
|
|
||||||
if variablesCircle[key] == true {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Info("key: %s, value: %s", key, value)
|
|
||||||
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta(key) + `\}|` + `\$` + regexp.QuoteMeta(key) + `\b`)
|
|
||||||
|
|
||||||
escapedValue := strings.ReplaceAll(value, `$`, `$$`)
|
|
||||||
command = re1.ReplaceAllString(command, escapedValue)
|
|
||||||
variablesName = append(variablesName, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var userSSHPublicKeyList []string
|
|
||||||
err = db.GetEngine(ctx).
|
|
||||||
Table("public_key").
|
|
||||||
Select("content").
|
|
||||||
Where("owner_id = ?", userId).
|
|
||||||
Find(&userSSHPublicKeyList)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\}|` + `\$` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\b`)
|
|
||||||
command = re1.ReplaceAllString(command, strings.Join(userSSHPublicKeyList, "\n"))
|
|
||||||
return command, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -385,16 +385,8 @@ func (c *DevContainerConfiguration) validateLifecycleCommands() []error {
|
|||||||
// 验证命令对象结构
|
// 验证命令对象结构
|
||||||
cmdObj := cmd.(map[string]interface{})
|
cmdObj := cmd.(map[string]interface{})
|
||||||
if _, ok := cmdObj["command"]; !ok {
|
if _, ok := cmdObj["command"]; !ok {
|
||||||
if pathValue, hasPath := cmdObj["path"]; !hasPath {
|
errors = append(errors,
|
||||||
errors = append(errors,
|
fmt.Errorf("%s: command object requires 'command' property", name))
|
||||||
fmt.Errorf("%s: command object requires either 'command' or 'path' property", name))
|
|
||||||
} else if hasPath {
|
|
||||||
// 如果存在 path,检查它是否为字符串
|
|
||||||
if _, ok := pathValue.(string); !ok {
|
|
||||||
errors = append(errors,
|
|
||||||
fmt.Errorf("%s: 'path' must be a string, got %T", name, pathValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
errors = append(errors,
|
errors = append(errors,
|
||||||
|
|||||||
@@ -233,69 +233,3 @@ func AddFileToTar(tw *tar.Writer, filename string, content string, mode int64) e
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func buildDependencyGraph(variables map[string]string) map[string][]string {
|
|
||||||
graph := make(map[string][]string)
|
|
||||||
varRefRegex := regexp.MustCompile(`\$[a-zA-Z_][a-zA-Z0-9_]*\b`)
|
|
||||||
|
|
||||||
for varName, varValue := range variables {
|
|
||||||
graph[varName] = []string{}
|
|
||||||
matches := varRefRegex.FindAllString(varValue, -1)
|
|
||||||
for _, match := range matches {
|
|
||||||
refVarName := strings.TrimPrefix(match, "$")
|
|
||||||
if _, exists := variables[refVarName]; exists {
|
|
||||||
graph[varName] = append(graph[varName], refVarName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return graph
|
|
||||||
}
|
|
||||||
|
|
||||||
func dfsDetectCycle(node string, graph map[string][]string, visited, inStack map[string]bool, path *[]string) bool {
|
|
||||||
visited[node] = true
|
|
||||||
inStack[node] = true
|
|
||||||
*path = append(*path, node)
|
|
||||||
|
|
||||||
for _, neighbor := range graph[node] {
|
|
||||||
if !visited[neighbor] {
|
|
||||||
if dfsDetectCycle(neighbor, graph, visited, inStack, path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else if inStack[neighbor] {
|
|
||||||
// Found cycle, complete the cycle path
|
|
||||||
*path = append(*path, neighbor)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inStack[node] = false
|
|
||||||
*path = (*path)[:len(*path)-1]
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func checkEachVariable(variables map[string]string) map[string]bool {
|
|
||||||
results := make(map[string]bool)
|
|
||||||
graph := buildDependencyGraph(variables)
|
|
||||||
|
|
||||||
for varName := range variables {
|
|
||||||
visited := make(map[string]bool)
|
|
||||||
inStack := make(map[string]bool)
|
|
||||||
var cyclePath []string
|
|
||||||
|
|
||||||
hasCycle := dfsDetectCycle(varName, graph, visited, inStack, &cyclePath)
|
|
||||||
results[varName] = hasCycle
|
|
||||||
|
|
||||||
if hasCycle {
|
|
||||||
fmt.Printf("变量 %s 存在循环引用: %v\n", varName, cyclePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
func ContainsAnySubstring(s string, substrList []string) bool {
|
|
||||||
for _, substr := range substrList {
|
|
||||||
hasSubstr, _ := regexp.MatchString(`\$`+substr+`\b`, s)
|
|
||||||
if hasSubstr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package devcontainer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
|
|
||||||
"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, description string) (*devcontainer_model.DevcontainerVariable, error) {
|
|
||||||
if err := secret_service.ValidateName(name); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := envNameCIRegexMatch(name); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := devcontainer_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateVariableNameData(ctx context.Context, variable *devcontainer_model.DevcontainerVariable) (bool, error) {
|
|
||||||
if err := secret_service.ValidateName(variable.Name); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := envNameCIRegexMatch(variable.Name); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
|
|
||||||
|
|
||||||
return devcontainer_model.UpdateVariableCols(ctx, variable, "name", "data", "description")
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteVariableByID(ctx context.Context, variableID int64) error {
|
|
||||||
return devcontainer_model.DeleteVariable(ctx, variableID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
|
|
||||||
if err := secret_service.ValidateName(name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := envNameCIRegexMatch(name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := GetVariable(ctx, devcontainer_model.FindVariablesOpts{
|
|
||||||
OwnerID: ownerID,
|
|
||||||
RepoID: repoID,
|
|
||||||
Name: name,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return devcontainer_model.DeleteVariable(ctx, v.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetVariable(ctx context.Context, opts devcontainer_model.FindVariablesOpts) (*devcontainer_model.DevcontainerVariable, error) {
|
|
||||||
vars, err := devcontainer_model.FindVariables(ctx, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(vars) != 1 {
|
|
||||||
return nil, util.NewNotExistErrorf("variable not found")
|
|
||||||
}
|
|
||||||
return vars[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -16,13 +16,10 @@ import (
|
|||||||
"code.gitea.io/gitea/models/repo"
|
"code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/user"
|
"code.gitea.io/gitea/models/user"
|
||||||
docker_module "code.gitea.io/gitea/modules/docker"
|
docker_module "code.gitea.io/gitea/modules/docker"
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
@@ -103,7 +100,7 @@ func CreateDevContainerByDockerAPI(ctx context.Context, newDevcontainer *devcont
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Info("ExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainer")
|
||||||
output, err := docker_module.ExecCommandInContainer(ctx, cli, newDevcontainer.Name,
|
output, err := docker_module.ExecCommandInContainer(ctx, cli, newDevcontainer.Name,
|
||||||
`echo "`+newDevcontainer.DevcontainerHost+` host.docker.internal" | tee -a /etc/hosts;apt update;apt install -y git ;git clone `+strings.TrimSuffix(setting.AppURL, "/")+repo.Link()+" "+newDevcontainer.DevcontainerWorkDir+"/"+repo.Name+`; apt install -y ssh;echo "PubkeyAuthentication yes `+"\n"+`PermitRootLogin yes `+"\n"+`" | tee -a /etc/ssh/sshd_config;rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart;mkdir -p ~/.ssh;chmod 700 ~/.ssh;echo "`+strings.Join(publicKeyList, "\n")+`" > ~/.ssh/authorized_keys;chmod 600 ~/.ssh/authorized_keys;`,
|
`echo "`+newDevcontainer.DevcontainerHost+` host.docker.internal" | tee -a /etc/hosts;apt update;apt install -y git ;git clone `+strings.TrimSuffix(setting.AppURL, "/")+repo.Link()+" "+newDevcontainer.DevcontainerWorkDir+"/"+repo.Name+`; apt install -y ssh;echo "PubkeyAuthentication yes `+"\n"+`PermitRootLogin yes `+"\n"+`" | tee -a /etc/ssh/sshd_config;rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart;mkdir -p ~/.ssh;chmod 700 ~/.ssh;echo "`+strings.Join(publicKeyList, "\n")+`" > ~/.ssh/authorized_keys;chmod 600 ~/.ssh/authorized_keys;`,
|
||||||
)
|
)
|
||||||
@@ -132,7 +129,6 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageName = configurationModel.Image
|
var imageName = configurationModel.Image
|
||||||
dockerSocket, err := docker_module.GetDockerSocketPath()
|
dockerSocket, err := docker_module.GetDockerSocketPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,7 +199,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return imageName, err
|
return imageName, err
|
||||||
}
|
}
|
||||||
var startCommand string = `docker -H ` + dockerSocket + ` create --restart=always --name ` + newDevcontainer.Name
|
var startCommand string = `docker -H ` + dockerSocket + ` run --restart=always -d --name ` + newDevcontainer.Name
|
||||||
|
|
||||||
// 将每个端口转换为 "-p <port>" 格式
|
// 将每个端口转换为 "-p <port>" 格式
|
||||||
var portFlags string = " -p 22 "
|
var portFlags string = " -p 22 "
|
||||||
@@ -213,12 +209,10 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
portFlags = portFlags + fmt.Sprintf(" -p %d ", port)
|
portFlags = portFlags + fmt.Sprintf(" -p %d ", port)
|
||||||
}
|
}
|
||||||
startCommand += portFlags
|
startCommand += portFlags
|
||||||
|
|
||||||
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
|
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
|
||||||
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
|
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
|
||||||
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` +
|
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `"` +
|
||||||
` -e DEVCONTAINER_STATUS="start" ` +
|
` -e PublicKeyList="` + strings.Join(publicKeyList, "\n") + `" `
|
||||||
` -e WEB_TERMINAL_HELLO="Successfully connected to the devcontainer" `
|
|
||||||
// 遍历 ContainerEnv 映射中的每个环境变量
|
// 遍历 ContainerEnv 映射中的每个环境变量
|
||||||
for name, value := range configurationModel.ContainerEnv {
|
for name, value := range configurationModel.ContainerEnv {
|
||||||
// 将每个环境变量转换为 "-e name=value" 格式
|
// 将每个环境变量转换为 "-e name=value" 格式
|
||||||
@@ -231,14 +225,15 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
if configurationModel.Privileged {
|
if configurationModel.Privileged {
|
||||||
startCommand += " --privileged "
|
startCommand += " --privileged "
|
||||||
}
|
}
|
||||||
|
|
||||||
var capAddFlags string
|
var capAddFlags string
|
||||||
// 遍历 CapAdd 列表中的每个能力
|
// 遍历 CapAdd 列表中的每个能力
|
||||||
for _, capability := range configurationModel.CapAdd {
|
for _, capability := range configurationModel.CapAdd {
|
||||||
// 将每个能力转换为 --cap-add=capability 格式
|
// 将每个能力转换为 --cap-add=capability 格式
|
||||||
capAddFlags = capAddFlags + fmt.Sprintf(" --cap-add %s ", capability)
|
capAddFlags = capAddFlags + fmt.Sprintf(" --cap-add %s ", capability)
|
||||||
}
|
}
|
||||||
|
|
||||||
startCommand += capAddFlags
|
startCommand += capAddFlags
|
||||||
|
|
||||||
var securityOptFlags string
|
var securityOptFlags string
|
||||||
// 遍历 SecurityOpt 列表中的每个安全选项
|
// 遍历 SecurityOpt 列表中的每个安全选项
|
||||||
for _, option := range configurationModel.SecurityOpt {
|
for _, option := range configurationModel.SecurityOpt {
|
||||||
@@ -251,31 +246,34 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
startCommand += fmt.Sprintf(" -w %s ", configurationModel.WorkspaceFolder)
|
startCommand += fmt.Sprintf(" -w %s ", configurationModel.WorkspaceFolder)
|
||||||
}
|
}
|
||||||
startCommand += " " + strings.Join(configurationModel.RunArgs, " ") + " "
|
startCommand += " " + strings.Join(configurationModel.RunArgs, " ") + " "
|
||||||
overrideCommand := ""
|
|
||||||
if !configurationModel.OverrideCommand {
|
|
||||||
overrideCommand = ` sh -c "/home/webTerminal.sh" `
|
|
||||||
startCommand += ` --entrypoint="" `
|
|
||||||
}
|
|
||||||
//创建并运行容器的命令
|
//创建并运行容器的命令
|
||||||
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
|
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
|
||||||
Output: "",
|
Output: "",
|
||||||
Status: "waitting",
|
Status: "waitting",
|
||||||
UserId: newDevcontainer.UserId,
|
UserId: newDevcontainer.UserId,
|
||||||
RepoId: newDevcontainer.RepoId,
|
RepoId: newDevcontainer.RepoId,
|
||||||
Command: startCommand + imageName + overrideCommand + "\n",
|
Command: startCommand + imageName + ` sh -c "tail -f /dev/null"` + "\n",
|
||||||
ListId: 2,
|
ListId: 2,
|
||||||
DevcontainerId: newDevcontainer.Id,
|
DevcontainerId: newDevcontainer.Id,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Info("Failed to insert record: %v", err)
|
log.Info("Failed to insert record: %v", err)
|
||||||
return imageName, err
|
return imageName, err
|
||||||
}
|
}
|
||||||
|
//安装基本工具的命令
|
||||||
|
onCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.OnCreateCommand), ";"))
|
||||||
|
if !strings.HasSuffix(onCreateCommand, ";") {
|
||||||
|
onCreateCommand += ";"
|
||||||
|
}
|
||||||
|
if onCreateCommand == ";" {
|
||||||
|
onCreateCommand = ""
|
||||||
|
}
|
||||||
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
|
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
|
||||||
Output: "",
|
Output: "",
|
||||||
Status: "waitting",
|
Status: "waitting",
|
||||||
UserId: newDevcontainer.UserId,
|
UserId: newDevcontainer.UserId,
|
||||||
RepoId: newDevcontainer.RepoId,
|
RepoId: newDevcontainer.RepoId,
|
||||||
Command: `docker -H ` + dockerSocket + ` start -a ` + newDevcontainer.Name + "\n",
|
Command: `docker -H ` + dockerSocket + ` exec ` + newDevcontainer.Name + ` /home/webTerminal.sh start; ` +
|
||||||
|
onCreateCommand + "\n",
|
||||||
ListId: 3,
|
ListId: 3,
|
||||||
DevcontainerId: newDevcontainer.Id,
|
DevcontainerId: newDevcontainer.Id,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -288,7 +286,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
Status: "waitting",
|
Status: "waitting",
|
||||||
UserId: newDevcontainer.UserId,
|
UserId: newDevcontainer.UserId,
|
||||||
RepoId: newDevcontainer.RepoId,
|
RepoId: newDevcontainer.RepoId,
|
||||||
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c 'echo "$WEB_TERMINAL_HELLO";bash'` + "\n",
|
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo 'Successfully connected to the container';bash"` + "\n",
|
||||||
ListId: 4,
|
ListId: 4,
|
||||||
DevcontainerId: newDevcontainer.Id,
|
DevcontainerId: newDevcontainer.Id,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -396,16 +394,17 @@ func StopDevContainerByDocker(ctx context.Context, devContainerName string) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *gitea_context.Repository, doer *user.User) error {
|
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *repo.Repository, doer *user.User) error {
|
||||||
// 创建docker client
|
// 创建docker client
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
cli, err := docker_module.CreateDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
// update容器
|
// update容器
|
||||||
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -415,45 +414,16 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updateInfo.SaveMethod == "on" {
|
if updateInfo.SaveMethod == "on" {
|
||||||
|
|
||||||
// 创建构建上下文(包含Dockerfile的tar包)
|
// 创建构建上下文(包含Dockerfile的tar包)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
defer tw.Close()
|
defer tw.Close()
|
||||||
// 添加Dockerfile到tar包
|
// 添加Dockerfile到tar包
|
||||||
var dockerfileContent string
|
|
||||||
dockerfile := "Dockerfile"
|
dockerfile := "Dockerfile"
|
||||||
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
|
||||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
|
||||||
if err != nil {
|
|
||||||
if git.IsErrNotExist(err) {
|
|
||||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/"+configurationModel.Build.Dockerfile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content := []byte(dockerfileContent)
|
content := []byte(dockerfileContent)
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
Name: dockerfile,
|
Name: dockerfile,
|
||||||
@@ -506,7 +476,7 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
|
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
|
||||||
// 使用正则表达式查找并替换 image 字段的值
|
// 使用正则表达式查找并替换 image 字段的值
|
||||||
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
|
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
|
||||||
err = UpdateDevcontainerConfiguration(newConfiguration, repo.Repository, doer)
|
err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -518,6 +488,7 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
// - bool: 镜像是否存在(true=存在,false=不存在)
|
// - bool: 镜像是否存在(true=存在,false=不存在)
|
||||||
// - error: 非空表示检查过程中发生错误
|
// - error: 非空表示检查过程中发生错误
|
||||||
func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
||||||
|
|
||||||
// 创建 Docker 客户端
|
// 创建 Docker 客户端
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
cli, err := docker_module.CreateDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -533,7 +504,6 @@ func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil // 镜像存在
|
return true, nil // 镜像存在
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string) (bool, error) {
|
func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string) (bool, error) {
|
||||||
// 上下文
|
// 上下文
|
||||||
// 创建 Docker 客户端
|
// 创建 Docker 客户端
|
||||||
@@ -552,47 +522,6 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string
|
|||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
}
|
}
|
||||||
// 创建 exec 实例
|
|
||||||
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行命令
|
|
||||||
var exitCode int
|
|
||||||
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取命令执行结果
|
|
||||||
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
exitCode = resp.ExitCode
|
|
||||||
return exitCode == 0, nil // 退出码为 0 表示目录存在
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) {
|
|
||||||
// 上下文
|
|
||||||
// 创建 Docker 客户端
|
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// 获取容器 ID
|
|
||||||
containerID, err := docker_module.GetContainerID(cli, containerName)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// 创建 exec 配置
|
|
||||||
execConfig := types.ExecConfig{
|
|
||||||
Cmd: []string{"test", "-e", filePath}, // 检查文件是否存在
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 exec 实例
|
// 创建 exec 实例
|
||||||
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
||||||
@@ -631,7 +560,7 @@ func RegistWebTerminal(ctx context.Context) error {
|
|||||||
// 拉取镜像
|
// 拉取镜像
|
||||||
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
|
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
return fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := time.Now().Format("20060102150405")
|
timestamp := time.Now().Format("20060102150405")
|
||||||
@@ -665,36 +594,3 @@ func RegistWebTerminal(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerExists 检查容器是否存在,返回存在状态和容器ID(如果存在)
|
|
||||||
func ContainerExists(ctx context.Context, containerName string) (bool, string, error) {
|
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
// 设置过滤器,根据容器名称过滤
|
|
||||||
filter := filters.NewArgs()
|
|
||||||
filter.Add("name", containerName)
|
|
||||||
|
|
||||||
// 获取容器列表,使用过滤器
|
|
||||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
|
|
||||||
All: true, // 包括所有容器(运行的和停止的)
|
|
||||||
Filters: filter,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历容器,检查名称是否完全匹配
|
|
||||||
for _, container := range containers {
|
|
||||||
for _, name := range container.Names {
|
|
||||||
// 容器名称在Docker API中是以斜杠开头的,例如 "/my-container"
|
|
||||||
// 所以我们需要检查去掉斜杠后的名称是否匹配
|
|
||||||
if strings.TrimPrefix(name, "/") == containerName {
|
|
||||||
return true, container.ID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, "", nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ type InstallForm struct {
|
|||||||
RequireSignInView bool
|
RequireSignInView bool
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
DefaultAllowCreateDevcontainer bool
|
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
EnableUpdateChecker bool
|
EnableUpdateChecker bool
|
||||||
NoReplyAddress string
|
NoReplyAddress string
|
||||||
|
|||||||
@@ -153,8 +153,6 @@
|
|||||||
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
|
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
||||||
<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
|
<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_devcontainer"}}</dt>
|
|
||||||
<dd>{{svg (Iif .Service.DefaultAllowCreateDevcontainer "octicon-check" "octicon-x")}}</dd>
|
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
|
<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
|
||||||
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
|
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
|
||||||
{{if .Service.EnableTimetracking}}
|
{{if .Service.EnableTimetracking}}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin actions")}}
|
|
||||||
<div class="admin-setting-content">
|
|
||||||
{{if eq .PageType "variables"}}
|
|
||||||
{{template "shared/devcontainer/variable_list" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{template "admin/layout_footer" .}}
|
|
||||||
@@ -112,14 +112,5 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsDevcontainerVariables}}open{{end}}>
|
|
||||||
<summary>{{ctx.Locale.Tr "admin.devcontainer"}}</summary>
|
|
||||||
<div class="menu">
|
|
||||||
<a class="{{if .PageIsSharedSettingsDevcontainerVariables}}active {{end}}item" href="{{AppSubUrl}}/-/admin/devcontainer/variables">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -304,12 +304,6 @@
|
|||||||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
|
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_devcontainer_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_devcontainer"}}</label>
|
|
||||||
<input name="default_allow_create_devcontainer" type="checkbox" {{if .default_allow_create_devcontainer}}checked{{end}}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
|
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings actions")}}
|
|
||||||
<div class="org-setting-content">
|
|
||||||
{{if eq .PageType "variables"}}
|
|
||||||
{{template "shared/devcontainer/variable_list" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{template "org/settings/layout_footer" .}}
|
|
||||||
@@ -41,13 +41,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{{end}}
|
{{end}}
|
||||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsDevcontainerVariables}}open{{end}}>
|
|
||||||
<summary>{{ctx.Locale.Tr "admin.devcontainer"}}</summary>
|
|
||||||
<div class="menu">
|
|
||||||
<a class="{{if .PageIsSharedSettingsDevcontainerVariables}}active {{end}}item" href="{{.OrgLink}}/settings/devcontainer/variables">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name":"template",
|
|
||||||
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
|
|
||||||
"forwardPorts": ["8080"],
|
|
||||||
"containerEnv": {
|
|
||||||
"NODE_ENV": "development"
|
|
||||||
},
|
|
||||||
"initializeCommand": "echo \"initializeCommand\";",
|
|
||||||
"onCreateCommand": [
|
|
||||||
"echo \"onCreateCommand\";",
|
|
||||||
"echo \"onCreateCommand\";"
|
|
||||||
],
|
|
||||||
"postCreateCommand": [
|
|
||||||
"echo \"postCreateCommand\"",
|
|
||||||
"echo \"OK\""
|
|
||||||
],
|
|
||||||
"postAttachCommand": [
|
|
||||||
"echo \"postAttachCommand\"",
|
|
||||||
"echo \"OK\""
|
|
||||||
],
|
|
||||||
"runArgs": [
|
|
||||||
"-p 8888"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
|
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
|
||||||
<form class="ui edit form">
|
<form class="ui edit form">
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="ui breadcrumb field">
|
<div class="ui breadcrumb field">
|
||||||
@@ -37,9 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
|
|
||||||
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
|
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,17 +44,17 @@
|
|||||||
|
|
||||||
<!-- 开始:Dev Container 正文内容 - 右侧展示区 -->
|
<!-- 开始:Dev Container 正文内容 - 右侧展示区 -->
|
||||||
<div class="issue-content-right ui segment">
|
<div class="issue-content-right ui segment">
|
||||||
<strong>{{ctx.Locale.Tr "repo.dev_container_control"}}</strong>
|
<strong>Options</strong>
|
||||||
<div class="ui relaxed list">
|
<div class="ui relaxed list">
|
||||||
|
|
||||||
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
|
{{if .HasDevContainer}}
|
||||||
<div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/devcontainer/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
|
<div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/devcontainer/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
|
||||||
{{if .isAdmin}}
|
{{if .isAdmin}}
|
||||||
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
|
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div style=" display: none;" id="restartContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}}{{ctx.Locale.Tr "repo.dev_container_control.start"}}</button></div>
|
<div style=" display: none;" id="restartContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
|
||||||
<div style=" display: none;" id="stopContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}}{{ctx.Locale.Tr "repo.dev_container_control.stop"}} </button></div>
|
<div style=" display: none;" id="stopContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></div>
|
||||||
|
|
||||||
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
|
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
|
||||||
<div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
|
<div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
|
||||||
@@ -69,7 +66,7 @@
|
|||||||
<div style=" display: none;" id="createContainer" class="item">
|
<div style=" display: none;" id="createContainer" class="item">
|
||||||
<div>
|
<div>
|
||||||
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
|
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
|
||||||
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_control.create"}}</button>
|
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,16 +84,6 @@
|
|||||||
<!-- 结束Dev Container 正文内容 -->
|
<!-- 结束Dev Container 正文内容 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 自定义警告框 -->
|
|
||||||
<div id="customAlert" class="custom-alert">
|
|
||||||
<div class="alert-content">
|
|
||||||
<div class="alert-header">
|
|
||||||
<strong>提示信息</strong>
|
|
||||||
<button class="alert-close" onclick="closeCustomAlert()">×</button>
|
|
||||||
</div>
|
|
||||||
<div id="alertText" class="alert-body"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 确认删除 Dev Container 模态对话框 -->
|
<!-- 确认删除 Dev Container 模态对话框 -->
|
||||||
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
||||||
@@ -109,14 +96,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "base/modal_actions_confirm" .}}
|
{{template "base/modal_actions_confirm" .}}
|
||||||
</div>
|
</div>
|
||||||
<!-- 保存 Dev Container 模态对话框 -->
|
<!-- 确认 Dev Container 模态对话框 -->
|
||||||
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
|
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
|
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
|
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
{{if not .HasDevContainerDockerfile}}
|
||||||
|
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
|
||||||
|
{{else}}
|
||||||
|
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
|
||||||
|
{{end}}
|
||||||
|
<label for="SaveMethod">Build From Dockerfile</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="required field ">
|
<div class="required field ">
|
||||||
<label for="RepositoryAddress">Registry:</label>
|
<label for="RepositoryAddress">Registry:</label>
|
||||||
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
|
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
|
||||||
@@ -127,38 +124,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="required field ">
|
<div class="required field ">
|
||||||
<label for="RepositoryPassword">Registry Password:</label>
|
<label for="RepositoryPassword">Registry Password:</label>
|
||||||
<div style="position: relative; display: inline-block; width: 100%;">
|
<input style="border: 1px solid black;" type="text" id="RepositoryPassword" name="RepositoryPassword" required>
|
||||||
<input style="border: 1px solid black; width: 100%; padding-right: 80px;"
|
|
||||||
type="password"
|
|
||||||
id="RepositoryPassword"
|
|
||||||
name="RepositoryPassword"
|
|
||||||
required
|
|
||||||
autocomplete="current-password">
|
|
||||||
<button type="button"
|
|
||||||
style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
|
|
||||||
background: none; border: none; cursor: pointer; color: #666;
|
|
||||||
font-size: 12px; padding: 5px 8px;"
|
|
||||||
onclick="togglePasswordVisibility('RepositoryPassword', this)">
|
|
||||||
显示密码
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="required field ">
|
<div class="required field ">
|
||||||
<label for="ImageName">Image(name:tag):</label>
|
<label for="ImageName">Image(name:tag):</label>
|
||||||
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
|
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
{{if not .HasDevContainerDockerfile}}
|
|
||||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
|
|
||||||
<label for="SaveMethod">There is no Dockerfile</label>
|
|
||||||
{{else}}
|
|
||||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
|
|
||||||
<label for="SaveMethod">Build From Dockerfile: {{.DockerfilePath}}</label>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
|
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
|
||||||
<button class="ui cancel button" id="updateCloseButton">Close</button>
|
<button class="ui cancel button" id="updateCloseButton">Close</button>
|
||||||
@@ -171,21 +143,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('updateSubmitButton').addEventListener('click', function() {
|
|
||||||
const form = document.getElementById('updateForm');
|
|
||||||
const formData = new FormData(form);
|
|
||||||
var RepositoryAddress = formData.get('RepositoryAddress');
|
|
||||||
var RepositoryUsername = formData.get('RepositoryUsername');
|
|
||||||
var RepositoryPassword = formData.get('RepositoryPassword');
|
|
||||||
var SaveMethod = formData.get('SaveMethod');
|
|
||||||
var ImageName = formData.get('ImageName');
|
|
||||||
if(ImageName != "" && SaveMethod != "" && RepositoryPassword != "" && RepositoryUsername != "" && RepositoryAddress != ""){
|
|
||||||
document.getElementById('updatemodal').classList.add('is-loading')
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var status = '-1'
|
var status = '-1'
|
||||||
var intervalID
|
var intervalID
|
||||||
const createContainer = document.getElementById('createContainer');
|
const createContainer = document.getElementById('createContainer');
|
||||||
@@ -273,16 +230,7 @@ function getStatus() {
|
|||||||
)
|
)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if(status !== '9' && status !== '-1' && data.status == '9'){
|
if (data.status == '-1' || data.status == '') {
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
else if(status !== '-1' && data.status == '-1'){
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
else if(status !== '4' && status !== '-1' && data.status == '4'){
|
|
||||||
//window.location.reload();
|
|
||||||
}
|
|
||||||
else if (data.status == '-1' || data.status == '') {
|
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
loadingElement.style.display = 'none';
|
loadingElement.style.display = 'none';
|
||||||
}
|
}
|
||||||
@@ -317,9 +265,6 @@ function getStatus() {
|
|||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
loadingElement.style.display = 'none';
|
loadingElement.style.display = 'none';
|
||||||
}
|
}
|
||||||
if (restartContainer) {
|
|
||||||
restartContainer.style.display = 'none';
|
|
||||||
}
|
|
||||||
clearInterval(intervalID);
|
clearInterval(intervalID);
|
||||||
}else if (data.status == '5') {
|
}else if (data.status == '5') {
|
||||||
concealElement();
|
concealElement();
|
||||||
@@ -369,34 +314,33 @@ function getStatus() {
|
|||||||
loadingElement.style.display = 'block';
|
loadingElement.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(status !== '9' && status !== '-1' && data.status == '9'){
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
if(status !== '-1' && data.status == '-1'){
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
status = data.status
|
status = data.status
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
intervalID = setInterval(getStatus, 5000);
|
intervalID = setInterval(getStatus, 3000);
|
||||||
if (restartContainer) {
|
if (restartContainer) {
|
||||||
restartContainer.addEventListener('click', function(event) {
|
restartContainer.addEventListener('click', function(event) {
|
||||||
// 处理点击逻辑
|
// 处理点击逻辑
|
||||||
concealElement();
|
concealElement();
|
||||||
if (loadingElement) {
|
|
||||||
loadingElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
|
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
|
||||||
.then(response => {intervalID = setInterval(getStatus, 5000);})
|
.then(response => {intervalID = setInterval(getStatus, 3000);})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (stopContainer) {
|
if (stopContainer) {
|
||||||
stopContainer.addEventListener('click', function(event) {
|
stopContainer.addEventListener('click', function(event) {
|
||||||
concealElement();
|
concealElement();
|
||||||
if (loadingElement) {
|
|
||||||
loadingElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
// 处理点击逻辑
|
// 处理点击逻辑
|
||||||
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
|
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
|
||||||
.then(response => {intervalID = setInterval(getStatus, 5000);})
|
.then(response => {intervalID = setInterval(getStatus, 3000);})
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -406,46 +350,10 @@ if (deleteContainer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function togglePasswordVisibility(passwordFieldId, button) {
|
|
||||||
const passwordInput = document.getElementById(passwordFieldId);
|
|
||||||
|
|
||||||
if (passwordInput.type === 'password') {
|
|
||||||
passwordInput.type = 'text';
|
|
||||||
button.textContent = '隐藏密码';
|
|
||||||
button.style.color = '#2185d0'; // 主色调,表示激活状态
|
|
||||||
} else {
|
|
||||||
passwordInput.type = 'password';
|
|
||||||
button.textContent = '显示密码';
|
|
||||||
button.style.color = '#666'; // 恢复默认颜色
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function showCustomAlert(message, title = "提示信息") {
|
|
||||||
const alertBox = document.getElementById('customAlert');
|
|
||||||
const alertText = document.getElementById('alertText');
|
|
||||||
const alertHeader = alertBox.querySelector('.alert-header strong');
|
|
||||||
|
|
||||||
alertHeader.textContent = title;
|
|
||||||
alertText.textContent = message;
|
|
||||||
alertBox.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCustomAlert() {
|
|
||||||
document.getElementById('customAlert').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击背景关闭
|
|
||||||
document.getElementById('customAlert').addEventListener('click', function(e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeCustomAlert();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function submitForm(event) {
|
function submitForm(event) {
|
||||||
event.preventDefault(); // 阻止默认的表单提交行为
|
event.preventDefault(); // 阻止默认的表单提交行为
|
||||||
const {csrfToken} = window.config;
|
const {csrfToken} = window.config;
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
const formModal = document.getElementById('updatemodal');
|
|
||||||
const form = document.getElementById('updateForm');
|
const form = document.getElementById('updateForm');
|
||||||
const submitButton = document.getElementById('updateSubmitButton');
|
const submitButton = document.getElementById('updateSubmitButton');
|
||||||
const closeButton = document.getElementById('updateCloseButton');
|
const closeButton = document.getElementById('updateCloseButton');
|
||||||
@@ -469,10 +377,9 @@ function submitForm(event) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
formModal.classList.remove('is-loading')
|
alert(data.message);
|
||||||
showCustomAlert(data.message);
|
|
||||||
if(data.redirect){
|
if(data.redirect){
|
||||||
closeCustomAlert()
|
closeButton.click()
|
||||||
}
|
}
|
||||||
intervalID = setInterval(getStatus, 3000);
|
intervalID = setInterval(getStatus, 3000);
|
||||||
})
|
})
|
||||||
@@ -502,69 +409,6 @@ function submitForm(event) {
|
|||||||
0%{-webkit-transform:rotate(0deg)}
|
0%{-webkit-transform:rotate(0deg)}
|
||||||
100%{-webkit-transform:rotate(360deg)}
|
100%{-webkit-transform:rotate(360deg)}
|
||||||
}
|
}
|
||||||
.custom-alert {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
.alert-content {
|
|
||||||
color: black;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: white;
|
|
||||||
padding: 0; /* 移除内边距,在内部元素中设置 */
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 80%;
|
|
||||||
max-width: 600px;
|
|
||||||
max-height: 80%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.alert-header {
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.alert-close {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #666;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.alert-close:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
.alert-body {
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(80vh - 100px); /* 减去头部高度 */
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
case $OS_ID in
|
|
||||||
ubuntu|debian)
|
|
||||||
service ssh restart;
|
|
||||||
;;
|
|
||||||
centos)
|
|
||||||
;;
|
|
||||||
fedora)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
failure "Unsupported OS: $OS_ID"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
# 重启服务的命令
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# 启动服务的命令
|
|
||||||
echo "$DevstarHost host.docker.internal" | tee -a /etc/hosts;
|
|
||||||
|
|
||||||
case $OS_ID in
|
|
||||||
ubuntu|debian)
|
|
||||||
apt-get update -y
|
|
||||||
# 检查 SSH 是否已安装
|
|
||||||
if ! dpkg -l | grep -q "^ii.*openssh-server"; then
|
|
||||||
echo "SSH 未安装,将进行安装"
|
|
||||||
apt-get install ssh -y
|
|
||||||
else
|
|
||||||
echo "SSH 已安装"
|
|
||||||
fi
|
|
||||||
# 检查 Git 是否已安装
|
|
||||||
if ! dpkg -l | grep -q "^ii.*git"; then
|
|
||||||
echo "Git 未安装,将进行安装"
|
|
||||||
apt-get install git -y
|
|
||||||
else
|
|
||||||
echo "Git 已安装"
|
|
||||||
fi
|
|
||||||
|
|
||||||
;;
|
|
||||||
centos)
|
|
||||||
# sudo yum update -y
|
|
||||||
# sudo yum install -y epel-release
|
|
||||||
# sudo yum groupinstall -y "Development Tools"
|
|
||||||
# sudo yum install -y yaml-cpp yaml-cpp-devel
|
|
||||||
;;
|
|
||||||
fedora)
|
|
||||||
# sudo dnf update -y
|
|
||||||
# sudo dnf group install -y "Development Tools"
|
|
||||||
# sudo dnf install -y yaml-cpp yaml-cpp-devel
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "PubkeyAuthentication yes\nPermitRootLogin yes\n" | tee -a /etc/ssh/sshd_config;
|
|
||||||
rm -f /etc/ssh/ssh_host_*;
|
|
||||||
ssh-keygen -A;
|
|
||||||
mkdir -p ~/.ssh;
|
|
||||||
chmod 700 ~/.ssh;
|
|
||||||
case $OS_ID in
|
|
||||||
ubuntu|debian)
|
|
||||||
service ssh restart;
|
|
||||||
;;
|
|
||||||
centos)
|
|
||||||
;;
|
|
||||||
fedora)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
failure "Unsupported OS: $OS_ID"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo "$PUBLIC_KEY_LIST" > ~/.ssh/authorized_keys;
|
|
||||||
chmod 600 ~/.ssh/authorized_keys
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# 获取参数
|
|
||||||
OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
|
|
||||||
|
|
||||||
# 根据参数执行不同命令
|
|
||||||
case $DEVCONTAINER_STATUS in
|
|
||||||
"restart")
|
|
||||||
echo "Restarting service..."
|
|
||||||
$RESTART
|
|
||||||
sh -c "tail -f /dev/null"
|
|
||||||
;;
|
|
||||||
"start")
|
|
||||||
echo "Starting service..."
|
|
||||||
$START
|
|
||||||
case $OS_ID in
|
|
||||||
ubuntu|debian)
|
|
||||||
echo 'DEVCONTAINER_STATUS="restart"' | tee -a /etc/environment
|
|
||||||
;;
|
|
||||||
centos)
|
|
||||||
;;
|
|
||||||
fedora)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
git clone $RepoLink $WorkSpace
|
|
||||||
sh -c "tail -f /dev/null"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {start|stop|restart}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings actions")}}
|
|
||||||
<div class="repo-setting-content">
|
|
||||||
{{if eq .PageType "variables"}}
|
|
||||||
{{template "shared/devcontainer/variable_list" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{template "repo/settings/layout_footer" .}}
|
|
||||||
@@ -54,13 +54,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{{end}}
|
{{end}}
|
||||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsDevcontainerVariables}}open{{end}}>
|
|
||||||
<summary>{{ctx.Locale.Tr "admin.devcontainer"}}</summary>
|
|
||||||
<div class="menu">
|
|
||||||
<a class="{{if .PageIsSharedSettingsDevcontainerVariables}}active {{end}}item" href="{{.RepoLink}}/settings/devcontainer/variables">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,363 +0,0 @@
|
|||||||
<h4 class="ui top attached header">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.scripts"}}
|
|
||||||
</h4>
|
|
||||||
<div class="ui attached segment">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.scripts.description"}}
|
|
||||||
{{if or .Variables .DevstarVariables}}
|
|
||||||
<div class="dynamic-tags" data-tags='{{.Tags}}'></div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<h4 class="ui top attached header">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables.management"}}
|
|
||||||
<div class="ui right">
|
|
||||||
<button class="ui primary tiny button show-modal"
|
|
||||||
data-modal="#edit-variable-modal"
|
|
||||||
data-modal-form.action="{{.Link}}/new"
|
|
||||||
data-modal-header="{{ctx.Locale.Tr "devcontainer.variables.creation"}}"
|
|
||||||
data-modal-dialog-variable-name=""
|
|
||||||
data-modal-dialog-variable-data=""
|
|
||||||
data-modal-dialog-variable-description=""
|
|
||||||
>
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables.creation"}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</h4>
|
|
||||||
<div class="ui attached segment">
|
|
||||||
{{if or .Variables .DevstarVariables}}
|
|
||||||
<div class="flex-list">
|
|
||||||
{{range .Variables}}
|
|
||||||
<div class="flex-item tw-items-center">
|
|
||||||
<div class="flex-item-leading">
|
|
||||||
{{svg "octicon-pencil" 32}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-main">
|
|
||||||
<div class="flex-item-title">
|
|
||||||
{{.Name}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-body">
|
|
||||||
{{if .Description}}{{.Description}}{{else}}-{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-body">
|
|
||||||
{{.Data}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-trailing">
|
|
||||||
<span class="color-text-light-2">
|
|
||||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
|
||||||
</span>
|
|
||||||
<button class="btn interact-bg tw-p-2 show-modal"
|
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "devcontainer.variables.edit"}}"
|
|
||||||
data-modal="#edit-variable-modal"
|
|
||||||
data-modal-form.action="{{$.Link}}/{{.ID}}/edit"
|
|
||||||
data-modal-header="{{ctx.Locale.Tr "devcontainer.variables.edit"}}"
|
|
||||||
data-modal-dialog-variable-name="{{.Name}}"
|
|
||||||
data-modal-dialog-variable-data="{{.Data}}"
|
|
||||||
data-modal-dialog-variable-description="{{.Description}}"
|
|
||||||
>
|
|
||||||
{{svg "octicon-pencil"}}
|
|
||||||
</button>
|
|
||||||
<button class="btn interact-bg tw-p-2 link-action"
|
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "devcontainer.variables.deletion"}}"
|
|
||||||
data-url="{{$.Link}}/{{.ID}}/delete"
|
|
||||||
data-modal-confirm="{{ctx.Locale.Tr "devcontainer.variables.deletion.description"}}"
|
|
||||||
>
|
|
||||||
{{svg "octicon-trash"}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{range .DevstarVariables}}
|
|
||||||
<div class="flex-item tw-items-center">
|
|
||||||
<div class="flex-item-leading">
|
|
||||||
{{svg "octicon-pencil" 32}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-main">
|
|
||||||
<div class="flex-item-title">
|
|
||||||
{{.Name}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-body">
|
|
||||||
{{if .Description}}{{.Description}}{{else}}-{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="flex-item-body">
|
|
||||||
{{.Data}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables.none"}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/** Edit variable dialog */}}
|
|
||||||
<div class="ui small modal" id="edit-variable-modal">
|
|
||||||
<div class="header"></div>
|
|
||||||
<form class="ui form form-fetch-action" method="post">
|
|
||||||
<div class="content">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<div class="field">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables.description"}}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="dialog-variable-name">{{ctx.Locale.Tr "name"}}</label>
|
|
||||||
<input autofocus required
|
|
||||||
name="name"
|
|
||||||
id="dialog-variable-name"
|
|
||||||
value="{{.name}}"
|
|
||||||
pattern="^(?!GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
|
|
||||||
placeholder="{{ctx.Locale.Tr "secrets.creation.name_placeholder"}}"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="dialog-variable-data">{{ctx.Locale.Tr "value"}}</label>
|
|
||||||
<textarea required
|
|
||||||
name="data"
|
|
||||||
id="dialog-variable-data"
|
|
||||||
placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="dialog-variable-description">{{ctx.Locale.Tr "secrets.creation.description"}}</label>
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
id="dialog-variable-description"
|
|
||||||
rows="2"
|
|
||||||
maxlength="{{.DescriptionMaxLength}}"
|
|
||||||
placeholder="{{ctx.Locale.Tr "secrets.creation.description_placeholder"}}"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function initDynamicTags() {
|
|
||||||
// 查找所有具有 dynamic-tags 类的元素
|
|
||||||
const elements = document.querySelectorAll('.dynamic-tags');
|
|
||||||
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 获取标签数据
|
|
||||||
let tags = [];
|
|
||||||
try {
|
|
||||||
tags = JSON.parse(el.getAttribute('data-tags') || '[]');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Invalid tags data:', el.getAttribute('data-tags'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建容器
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'dynamic-tags-container';
|
|
||||||
|
|
||||||
// 创建标签列表容器
|
|
||||||
const tagList = document.createElement('div');
|
|
||||||
tagList.className = 'dynamic-tags-list';
|
|
||||||
|
|
||||||
// 渲染标签
|
|
||||||
function renderTags() {
|
|
||||||
// 清空标签列表
|
|
||||||
tagList.innerHTML = '';
|
|
||||||
|
|
||||||
// 添加每个标签
|
|
||||||
tags.forEach(tag => {
|
|
||||||
const tagElement = document.createElement('span');
|
|
||||||
tagElement.className = 'tag-item';
|
|
||||||
tagElement.innerHTML = `
|
|
||||||
${tag}
|
|
||||||
<button class="tag-close" data-tag="${tag}">×</button>
|
|
||||||
`;
|
|
||||||
tagList.appendChild(tagElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加"新增标签"按钮或输入框
|
|
||||||
const inputContainer = document.createElement('span');
|
|
||||||
inputContainer.className = 'tag-input-container';
|
|
||||||
inputContainer.innerHTML = `
|
|
||||||
<button class="tag-add-button">+ New Script</button>
|
|
||||||
<input type="text" class="tag-input" style="display: none;" placeholder="Enter tag">
|
|
||||||
`;
|
|
||||||
|
|
||||||
tagList.appendChild(inputContainer);
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
function bindEvents() {
|
|
||||||
// 删除标签事件
|
|
||||||
tagList.querySelectorAll('.tag-close').forEach(button => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
const tag = e.target.getAttribute('data-tag');
|
|
||||||
// 删除标签时访问 /script/delete
|
|
||||||
fetch('{{.Link}}/script/delete?name=' + encodeURIComponent(tag), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
tags = tags.filter(t => t !== tag);
|
|
||||||
renderTags();
|
|
||||||
console.log('Successfully deleted script for variable: ' + tag);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to delete script for variable: ' + tag);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Error deleting script:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示输入框事件
|
|
||||||
const addButton = tagList.querySelector('.tag-add-button');
|
|
||||||
const tagInput = tagList.querySelector('.tag-input');
|
|
||||||
|
|
||||||
addButton.addEventListener('click', () => {
|
|
||||||
addButton.style.display = 'none';
|
|
||||||
tagInput.style.display = 'inline-block';
|
|
||||||
tagInput.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加标签事件
|
|
||||||
tagInput.addEventListener('keyup', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const value = e.target.value.trim();
|
|
||||||
if (value && !tags.includes(value)) {
|
|
||||||
// 当按下 Enter 键时,发送请求到 /script/new
|
|
||||||
fetch('{{.Link}}/script/new?name=' + encodeURIComponent(value), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Successfully created script for variable: ' + value);
|
|
||||||
tags.push(value);
|
|
||||||
renderTags();
|
|
||||||
} else {
|
|
||||||
console.error('Failed to create script for variable: ' + value);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Error creating script:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 失去焦点时隐藏输入框
|
|
||||||
tagInput.addEventListener('blur', () => {
|
|
||||||
const value = tagInput.value.trim();
|
|
||||||
if (value && !tags.includes(value)) {
|
|
||||||
fetch('{{.Link}}/script/new?name=' + encodeURIComponent(value), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Successfully created script for variable: ' + value);
|
|
||||||
tags.push(value);
|
|
||||||
renderTags();
|
|
||||||
} else {
|
|
||||||
console.error('Failed to create script for variable: ' + value);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Error creating script:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addButton.style.display = 'inline-flex';
|
|
||||||
tagInput.style.display = 'none';
|
|
||||||
tagInput.value = '';
|
|
||||||
renderTags();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始渲染
|
|
||||||
renderTags();
|
|
||||||
container.appendChild(tagList);
|
|
||||||
el.innerHTML = '';
|
|
||||||
el.appendChild(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
initDynamicTags();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.dynamic-tags-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-close {
|
|
||||||
margin-left: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.8;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-close:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-add-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px dashed #6c757d;
|
|
||||||
color: #6c757d;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-add-button:hover {
|
|
||||||
border-color: #007bff;
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input-container {
|
|
||||||
display: inline-block;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
border: 1px solid #6c757d;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #007bff;
|
|
||||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings actions")}}
|
|
||||||
<div class="user-setting-content">
|
|
||||||
{{if eq .PageType "variables"}}
|
|
||||||
{{template "shared/devcontainer/variable_list" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{template "user/settings/layout_footer" .}}
|
|
||||||
@@ -65,13 +65,5 @@
|
|||||||
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
||||||
{{ctx.Locale.Tr "settings.repos"}}
|
{{ctx.Locale.Tr "settings.repos"}}
|
||||||
</a>
|
</a>
|
||||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsDevcontainerVariables}}open{{end}}>
|
|
||||||
<summary>{{ctx.Locale.Tr "admin.devcontainer"}}</summary>
|
|
||||||
<div class="menu">
|
|
||||||
<a class="{{if .PageIsSharedSettingsDevcontainerVariables}}active {{end}}item" href="{{AppSubUrl}}/user/settings/devcontainer/variables">
|
|
||||||
{{ctx.Locale.Tr "devcontainer.variables"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
70
webTerminal.sh
Normal file
70
webTerminal.sh
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 获取参数
|
||||||
|
ACTION=$1
|
||||||
|
OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 根据参数执行不同命令
|
||||||
|
case $ACTION in
|
||||||
|
"start")
|
||||||
|
echo "Starting service..."
|
||||||
|
# 启动服务的命令
|
||||||
|
echo "$DevstarHost host.docker.internal" | tee -a /etc/hosts;
|
||||||
|
case $OS_ID in
|
||||||
|
ubuntu|debian)
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install ssh git -y
|
||||||
|
;;
|
||||||
|
centos)
|
||||||
|
# sudo yum update -y
|
||||||
|
# sudo yum install -y epel-release
|
||||||
|
# sudo yum groupinstall -y "Development Tools"
|
||||||
|
# sudo yum install -y yaml-cpp yaml-cpp-devel
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
# sudo dnf update -y
|
||||||
|
# sudo dnf group install -y "Development Tools"
|
||||||
|
# sudo dnf install -y yaml-cpp yaml-cpp-devel
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
failure "Unsupported OS: $OS_ID"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "PubkeyAuthentication yes\nPermitRootLogin yes\n" | tee -a /etc/ssh/sshd_config;
|
||||||
|
rm -f /etc/ssh/ssh_host_*;
|
||||||
|
ssh-keygen -A;
|
||||||
|
mkdir -p ~/.ssh;
|
||||||
|
chmod 700 ~/.ssh;
|
||||||
|
case $OS_ID in
|
||||||
|
ubuntu|debian)
|
||||||
|
service ssh restart;
|
||||||
|
;;
|
||||||
|
centos)
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
failure "Unsupported OS: $OS_ID"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "$PublicKeyList" > ~/.ssh/authorized_keys;
|
||||||
|
chmod 600 ~/.ssh/authorized_keys
|
||||||
|
git clone $RepoLink $WorkSpace
|
||||||
|
;;
|
||||||
|
"stop")
|
||||||
|
echo "Stopping service..."
|
||||||
|
# 停止服务的命令
|
||||||
|
;;
|
||||||
|
"restart")
|
||||||
|
echo "Restarting service..."
|
||||||
|
# 重启服务的命令
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user