~/Projects/duck-ci
git clone https://code.lsong.org/duck-ci
Commit
- Commit
- 241c4bf76b73a5541d3677a56d03fcd493f138d0
- Author
- Lsong <[email protected]>
- Date
- 2023-03-31 17:18:02 +0800 +0800
- Diffstat
.gitignore | 3 + docker.go | 34 ++++++++++++ git.go | 9 +++ go.mod | 5 + go.sum | 2 job.go | 33 ++++++++++++ main.go | 31 +++++++++++ server.go | 117 ++++++++++++++++++++++++++++++++++++++++++++ storage.go | 111 +++++++++++++++++++++++++++++++++++++++++ templates/index.html | 17 ++++++ templates/new.html | 25 +++++++++ templates/project.html | 31 +++++++++++
update
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..24ec673def80242412004a8da12d7ebe0713ce21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + + +*.db \ No newline at end of file diff --git a/docker.go b/docker.go new file mode 100644 index 0000000000000000000000000000000000000000..83d3827ed74fdf498464663bfb4e75d155fee92b --- /dev/null +++ b/docker.go @@ -0,0 +1,34 @@ +package main + +type Docker struct { +} + +type Command struct { + Cmd string + Args []string +} + +func (d Docker) BuildImage(dockerfile string) { + +} + +func (d Docker) RemoveImage() { + +} + +func (d Docker) CreateContainer() { + +} + +func (d Docker) RemoveContainer() { + +} + +func (d Docker) Run(cmd Command) { + // container := d.CreateContainer() + // container.Run() +} + +func (d Docker) Status() { + +} diff --git a/git.go b/git.go new file mode 100644 index 0000000000000000000000000000000000000000..adf78fbf6a190ee63ca5fe2aa60db003e1ae1e01 --- /dev/null +++ b/git.go @@ -0,0 +1,9 @@ +package main + +func cloneRepo(project *Project) (repoPath string, err error) { + return +} + +func parseSteps(config string) (steps []Step, err error) { + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..4fb9bf492fce9a4a6139c07bd8c7618a179eec37 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/song940/duckci + +go 1.20 + +require github.com/mattn/go-sqlite3 v1.14.16 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..7878efc1cd05804242ffb7a9f6b810ca7fcf88d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= diff --git a/job.go b/job.go new file mode 100644 index 0000000000000000000000000000000000000000..0f1258a80cd727ec4bfb1fe018bf92f6adb550b9 --- /dev/null +++ b/job.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "time" +) + +type Job struct { + Id uint32 `json:"id"` + Project *Project `json:"project"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +type Step struct { + Name string + Image string + Run string +} + +func NewJob(project *Project) (job *Job) { + job = &Job{Project: project} + return +} + +func (job *Job) Run() { + repoPath, _ := cloneRepo(job.Project) + steps, _ := parseSteps(repoPath) + for _, step := range steps { + log.Println("Job Run", step) + // job.docker.Run(step) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..49ff6931c81155a96c59c888aed3f129b8db1920 --- /dev/null +++ b/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" +) + +var ( + port string +) + +func main() { + + flag.StringVar(&port, "port", "4000", "http port") + flag.Parse() + + config := DuckCIConfig{ + Database: "duckci.db", + } + ci, err := New(config) + if err != nil { + log.Fatal(err) + } + http.HandleFunc("/", ci.IndexView) + http.HandleFunc("/new", ci.ProjectView) + http.HandleFunc("/projects", ci.ProjectView) + http.HandleFunc("/task", ci.TaskView) + http.ListenAndServe(fmt.Sprintf(":%s", port), nil) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000000000000000000000000000000000000..a3241749cf185507d69607f3dcacc0da6936fdf7 --- /dev/null +++ b/server.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "net/http" +) + +type DuckCI struct { + db *Storage +} + +type DuckCIConfig struct { + Database string +} + +type Project struct { + Id uint32 `json:"id"` + Name string `json:"name"` + Repo string `json:"repo"` + Branch string `json:"branch"` +} + +type H map[string]interface{} + +func New(config DuckCIConfig) (ci *DuckCI, err error) { + storage, err := NewStorage(config.Database) + if err != nil { + return + } + ci = &DuckCI{ + db: storage, + } + storage.Init() + return +} + +func (ci *DuckCI) Render(w http.ResponseWriter, name string, data H) { + tmpl, err := template.ParseFiles(fmt.Sprintf("templates/%s.html", name)) + if err != nil { + log.Println("Error parsing template:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + err = tmpl.Execute(w, data) + if err != nil { + log.Println("Error executing template:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } +} + +func (ci *DuckCI) IndexView(w http.ResponseWriter, r *http.Request) { + projects, err := ci.db.listProjects() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ci.Render(w, "index", H{ + "projects": projects, + }) +} + +func (ci *DuckCI) ProjectView(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && r.URL.Path == "/new" { + ci.Render(w, "new", H{}) + return + } + if r.Method == http.MethodPost && r.URL.Path == "/projects" { + r.ParseForm() + name := r.Form.Get("name") + repo := r.Form.Get("repo") + branch := r.Form.Get("branch") + project, err := ci.db.createProject(name, repo, branch) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, fmt.Sprintf("/projects?id=%d", project.Id), http.StatusFound) + } + if r.Method == "GET" && r.URL.Path == "/projects" { + projectId := r.URL.Query().Get("id") + project, _ := ci.db.getProjectById(projectId) + tasks, err := ci.db.getJobsByProjectId(project.Id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ci.Render(w, "project", H{ + "project": project, + "tasks": tasks, + }) + return + } +} + +func (ci *DuckCI) TaskView(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + r.ParseForm() + projectId := r.FormValue("project") + project, err := ci.db.getProjectById(projectId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + job, err := ci.db.createJob(project) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + go job.Run() + http.Redirect(w, r, fmt.Sprintf("/projects?id=%d", project.Id), http.StatusFound) + } +} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..816ee72bec34f33218be658047e253f81a04ba87 --- /dev/null +++ b/storage.go @@ -0,0 +1,111 @@ +package main + +import ( + "database/sql" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +type Storage struct { + db *sql.DB +} + +func NewStorage(path string) (storage *Storage, err error) { + db, err := sql.Open("sqlite3", path) + storage = &Storage{db} + return +} + +func (s *Storage) Init() error { + sql := ` + create table projects ( + id integer not null primary key, + name text not null, + repo text not null, + branch text not null + ); + create table jobs ( + id integer not null primary key, + project_id integer not null, + status integer not null, + created_at timestamp default CURRENT_TIMESTAMP, + foreign key (project_id) references projects(id) + ); + ` + _, err := s.db.Exec(sql) + return err +} + +func (s *Storage) createProject(name string, repo string, branch string) (project *Project, err error) { + project = &Project{ + Name: name, + Repo: repo, + Branch: branch, + } + sql := ` + INSERT INTO projects ( + name, repo, branch + ) + VALUES ( + ?, ?, ? + ) + RETURNING id + ` + err = s.db.QueryRow(sql, name, repo, branch).Scan(&project.Id) + return +} + +func (s *Storage) listProjects() (projects []Project, err error) { + sql := `SELECT * FROM projects` + rows, err := s.db.Query(sql) + if err != nil { + return + } + defer rows.Close() + + var project Project + for rows.Next() { + err := rows.Scan(&project.Id, &project.Name, &project.Repo, &project.Branch) + if err != nil { + log.Fatal(err) + } + projects = append(projects, project) + } + return +} + +func (s *Storage) getProjectById(id string) (project *Project, err error) { + project = &Project{} + sql := `SELECT * FROM projects WHERE id = ?` + err = s.db.QueryRow(sql, id).Scan(&project.Id, &project.Name, &project.Repo, &project.Branch) + return +} + +func (s *Storage) createJob(project *Project) (job *Job, err error) { + job = NewJob(project) + sql := ` + INSERT INTO jobs ( + project_id, + status + ) + VALUES (?, ?) + RETURNING id + ` + err = s.db.QueryRow(sql, project.Id, 0).Scan(&job.Id) + return +} + +func (s *Storage) getJobsByProjectId(id uint32) (jobs []Job, err error) { + sql := `select id, status, created_at from jobs where project_id = ?` + rows, err := s.db.Query(sql, id) + var job Job + for rows.Next() { + err = rows.Scan(&job.Id, &job.Status, &job.CreatedAt) + if err != nil { + return + } + jobs = append(jobs, job) + } + return +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..5372ef636ef7b9fb62fc0cb3c0add1adc12c6710 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,17 @@ + +<nav> + <a href="/">Index</a> + <a href="/new">New Project</a> +</nav> + +<h2>Projects</h2> + +<ul> + {{range .projects}} + <li> + <a href="/projects?id={{.Id}}">{{.Name}}</a> + <p>{{.Repo}}</p> + <p>{{.Branch}}</p> + </li> + {{end}} +</ul> \ No newline at end of file diff --git a/templates/new.html b/templates/new.html new file mode 100644 index 0000000000000000000000000000000000000000..a61b68cabfd799336b011848180fa7aae12b1faf --- /dev/null +++ b/templates/new.html @@ -0,0 +1,25 @@ + +<nav> + <a href="/">Index</a> + <a href="/new">New Project</a> +</nav> + +<h2>Create Project</h2> + +<form method="post" action="/projects"> + <div class="form-field"> + <label for="title">Name:</label> + <input type="text" name="name" id="title"> + </div> + <div class="form-field"> + <label for="repo">Repo:</label> + <input type="text" name="repo" id="repo"> + </div> + <div class="form-field"> + <label for="branch">Branch:</label> + <input type="text" name="branch" id="branch" value="master"> + </div> + <div class="form-field"> + <button class="button button-primary">create</button> + </div> +</form> \ No newline at end of file diff --git a/templates/project.html b/templates/project.html new file mode 100644 index 0000000000000000000000000000000000000000..fa7f3d3c80e7ec1d46abbc87602cabb53b892760 --- /dev/null +++ b/templates/project.html @@ -0,0 +1,31 @@ + +<nav> + <a href="/">Index</a> + <a href="/new">New Project</a> +</nav> + +<h2>{{.project.Name}}</h2> + +<p>{{.project.Repo}}</p> +<p>{{.project.Branch}}</p> + +<div> + <h3>Create Task</h3> + <form method="post" action="/task"> + <input name="project" type="hidden" value="{{.project.Id}}"> + <button>create</button> + </form> +</div> + +<div> + <h3>Tasks</h3> + <ul> + {{range .tasks}} + <li> + <a href="/task/{{.Id}}">Task #{{.Id}}</a> + <p>Status: {{.Status}}</p> + <p>Created At: {{.CreatedAt}}</p> + </li> + {{end}} + </ul> +</div> \ No newline at end of file