diff --git a/cmd/images.go b/cmd/images.go new file mode 100644 index 0000000..0db276f --- /dev/null +++ b/cmd/images.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + "net/http" +) + +func RegistryImages(app *app.App) { + type catalog struct { + Repositories []string `json:"repositories"` + } + + resp, err := http.Get(app.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) + } + } +} diff --git a/cmd/push.go b/cmd/push.go new file mode 100644 index 0000000..e2f5008 --- /dev/null +++ b/cmd/push.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + "os" + "os/exec" + "strings" +) + +func RegistryPush(app *app.App, imageName string) { + if !strings.HasPrefix(imageName, app.RegistryName) { + imageName = app.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) + } +} diff --git a/cmd/rm.go b/cmd/rm.go new file mode 100644 index 0000000..c5109a2 --- /dev/null +++ b/cmd/rm.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + "net/http" + "strings" +) + +func RegistryRm(app *app.App, imageArg string) { + parts := strings.SplitN(imageArg, ":", 2) + imageName := parts[0] + tag := "" + if len(parts) > 1 { + tag = parts[1] + } + + if tag == "" { + deleteImageByName(app, imageName) + } else { + deleteImageByTag(app, imageName, tag) + } +} + +func deleteImageByName(app *app.App, imageName string) { + type tagList struct { + Tags []string `json:"tags"` + } + + resp, err := http.Get(fmt.Sprintf("%s/v2/%s/tags/list", app.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(app, imageName, tag) + } +} + +func deleteImageByTag(app *app.App, imageName, tag string) { + client := &http.Client{} + + req, err := http.NewRequest("HEAD", fmt.Sprintf("%s/v2/%s/manifests/%s", app.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", app.RegistryURL, imageName, digest), nil) + if err != nil { + fmt.Println("Error creating delete request:", err) + return + } + + // Join username and password for Basic Auth + registryCredentials := app.RegistryUsername + ":" + app.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) + } +} diff --git a/cmd/tag.go b/cmd/tag.go new file mode 100644 index 0000000..1936e8a --- /dev/null +++ b/cmd/tag.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + "os" + "os/exec" + "strings" +) + +func RegistryTag(app *app.App, sourceImage, targetImage string) { + if !strings.HasPrefix(targetImage, app.RegistryName) { + targetImage = app.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) + } +} diff --git a/cmd/tags.go b/cmd/tags.go new file mode 100644 index 0000000..9214cbf --- /dev/null +++ b/cmd/tags.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + "net/http" +) + +func RegistryTags(app *app.App, imageName string) { + type tagList struct { + Tags []string `json:"tags"` + } + + resp, err := http.Get(fmt.Sprintf("%s/v2/%s/tags/list", app.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) + } + } +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..15fa583 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,33 @@ +package app + +import ( + "fmt" + "os" +) + +type App struct { + RegistryName string + RegistryURL string + RegistryUsername string + RegistryPassword string +} + +func NewApp() (*App, error) { + registryName := os.Getenv("REGISTRY_NAME") + if registryName == "" { + return nil, fmt.Errorf("REGISTRY_NAME not found in .env") + } + + registryUsername := os.Getenv("REGISTRY_USERNAME") + registryPassword := os.Getenv("REGISTRY_PASSWORD") + if registryUsername == "" || registryPassword == "" { + return nil, fmt.Errorf("REGISTRY_USERNAME or REGISTRY_PASSWORD not found in .env") + } + + return &App{ + RegistryName: registryName, + RegistryURL: "https://" + registryName, + RegistryUsername: registryUsername, + RegistryPassword: registryPassword, + }, nil +} diff --git a/main.go b/main.go index af1ab3b..a7d2e8a 100644 --- a/main.go +++ b/main.go @@ -1,46 +1,30 @@ package main import ( - "encoding/base64" - "encoding/json" "fmt" - "github.com/joho/godotenv" - "net/http" "os" - "os/exec" "path/filepath" "runtime" - "strings" -) -var ( - registryName string - registryURL string - registryUsername string - registryPassword string + "gitea.stuzer.link/stuzer05/docker-registry-manager/cmd" + "gitea.stuzer.link/stuzer05/docker-registry-manager/internal/app" + + "github.com/joho/godotenv" ) func main() { // Load .env file - _, b, _, _ := runtime.Caller(0) // @refactor + _, b, _, _ := runtime.Caller(0) err := godotenv.Load(filepath.Join(filepath.Dir(b), ".env")) 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") + // Initialize app + app, err := app.NewApp() + if err != nil { + fmt.Println("Error initializing app:", err) return } @@ -51,213 +35,32 @@ func main() { switch os.Args[1] { case "images": - registryImages() + cmd.RegistryImages(app) case "tags": if len(os.Args) < 3 { fmt.Println("Error: Please provide an image name.") return } - registryTags(os.Args[2]) + cmd.RegistryTags(app, 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]) + cmd.RegistryRm(app, 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]) + cmd.RegistryPush(app, 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]) + cmd.RegistryTag(app, 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) - } -}