diff --git a/.env.example b/.env.example index 4272213..4ecc5bd 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ -REGISTRY_NAME=registry.docker.stuzer.link -REGISTRY_USERNAME=stuzer05 -REGISTRY_PASSWORD=rnxt0FZxqXiXsExNTkMd \ No newline at end of file +REGISTRY_NAME=registry.example.com +REGISTRY_USERNAME=user +REGISTRY_PASSWORD=password \ No newline at end of file diff --git a/.gitignore b/.gitignore index e69de29..ef93166 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/.env +/docker-registry-manager \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c47bf05 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build fmt lint + +build: fmt lint + go build . + +fmt: + gofmt -w -r "interface{} -> any" . + go fmt ./... + +lint: + go vet ./... + staticcheck ./... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4092439 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitea.stuzer.link/stuzer05/docker-registry-manager + +go 1.23.0 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a7e558d --- /dev/null +++ b/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "github.com/joho/godotenv" + "net/http" + "os" + "os/exec" + "strings" +) + +var ( + registryName string + registryURL string + registryUsername string + registryPassword string +) + +func main() { + // Load .env file + err := godotenv.Load() + if err != nil { + fmt.Println("Error loading .env file:", err) + return + } + + // Read values from environment + registryName = os.Getenv("REGISTRY_NAME") + if registryName == "" { + fmt.Println("REGISTRY_NAME not found in .env") + return + } + + registryURL = "https://" + registryName + registryUsername = os.Getenv("REGISTRY_USERNAME") + registryPassword = os.Getenv("REGISTRY_PASSWORD") + if registryUsername == "" || registryPassword == "" { + fmt.Println("REGISTRY_USERNAME or REGISTRY_PASSWORD not found in .env") + return + } + + if len(os.Args) < 2 { + fmt.Println("Invalid command. Usage: registry [images|tags|rm|push|tag] ") + return + } + + switch os.Args[1] { + case "images": + registryImages() + case "tags": + if len(os.Args) < 3 { + fmt.Println("Error: Please provide an image name.") + return + } + registryTags(os.Args[2]) + case "rm": + if len(os.Args) < 3 { + fmt.Println("Error: Please provide an image name and optionally a tag (e.g., my-image or my-image:latest).") + return + } + registryRm(os.Args[2]) + case "push": + if len(os.Args) < 3 { + fmt.Println("Error: Please provide an image name to push.") + return + } + registryPush(os.Args[2]) + case "tag": + if len(os.Args) < 4 { + fmt.Println("Error: Please provide both source and target image names.") + return + } + registryTag(os.Args[2], os.Args[3]) + default: + fmt.Println("Invalid command. Usage: registry [images|tags|rm|push|tag] ") + } +} + +func registryImages() { + type catalog struct { + Repositories []string `json:"repositories"` + } + + resp, err := http.Get(registryURL + "/v2/_catalog") + if err != nil { + fmt.Println("Error fetching images:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + fmt.Println("No images found in the registry.") + return + } + + var cat catalog + if err := json.NewDecoder(resp.Body).Decode(&cat); err != nil { + fmt.Println("Error decoding response:", err) + return + } + + if len(cat.Repositories) == 0 { + fmt.Println("No images found in the registry.") + } else { + for _, img := range cat.Repositories { + fmt.Println(img) + } + } +} + +func registryTags(imageName string) { + type tagList struct { + Tags []string `json:"tags"` + } + + resp, err := http.Get(fmt.Sprintf("%s/v2/%s/tags/list", registryURL, imageName)) + if err != nil { + fmt.Println("Error fetching tags:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + fmt.Printf("Error: Image '%s' not found in the registry.\n", imageName) + return + } + + var tl tagList + if err := json.NewDecoder(resp.Body).Decode(&tl); err != nil { + fmt.Println("Error decoding response:", err) + return + } + + if len(tl.Tags) == 0 { + fmt.Printf("No tags found for image: %s\n", imageName) + } else { + for _, tag := range tl.Tags { + fmt.Println(tag) + } + } +} + +func registryRm(imageArg string) { + parts := strings.SplitN(imageArg, ":", 2) + imageName := parts[0] + tag := "" + if len(parts) > 1 { + tag = parts[1] + } + + if tag == "" { + deleteImageByName(imageName) + } else { + deleteImageByTag(imageName, tag) + } +} + +func deleteImageByName(imageName string) { + type tagList struct { + Tags []string `json:"tags"` + } + + resp, err := http.Get(fmt.Sprintf("%s/v2/%s/tags/list", registryURL, imageName)) + if err != nil { + fmt.Println("Error fetching tags:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + fmt.Printf("Error: Image '%s' not found in the registry.\n", imageName) + return + } + + var tl tagList + if err := json.NewDecoder(resp.Body).Decode(&tl); err != nil { + fmt.Println("Error decoding response:", err) + return + } + + for _, tag := range tl.Tags { + deleteImageByTag(imageName, tag) + } +} + +func deleteImageByTag(imageName, tag string) { + client := &http.Client{} + + req, err := http.NewRequest("HEAD", fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL, imageName, tag), nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") + + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error fetching manifest:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + fmt.Printf("Error: Image '%s:%s' not found in the registry.\n", imageName, tag) + return + } + + digest := resp.Header.Get("Docker-Content-Digest") + + req, err = http.NewRequest("DELETE", fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL, imageName, digest), nil) + if err != nil { + fmt.Println("Error creating delete request:", err) + return + } + + // Join username and password for Basic Auth + registryCredentials := registryUsername + ":" + registryPassword + req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(registryCredentials))) + + resp, err = client.Do(req) + if err != nil { + fmt.Println("Error deleting image:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusAccepted { + fmt.Printf("Deleted image: %s:%s\n", imageName, tag) + } else { + fmt.Printf("Failed to delete image: %s:%s (status code: %d)\n", imageName, tag, resp.StatusCode) + } +} + +func registryPush(imageName string) { + if !strings.HasPrefix(imageName, registryName) { + imageName = registryName + "/" + imageName + } + + cmd := exec.Command("docker", "push", imageName) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Println("Error pushing image:", err) + } +} + +func registryTag(sourceImage, targetImage string) { + if !strings.HasPrefix(targetImage, registryName) { + targetImage = registryName + "/" + targetImage + } + + cmd := exec.Command("docker", "tag", sourceImage, targetImage) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Println("Error tagging image:", err) + } +}