Liu Song’s Projects


~/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