Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added creation date to fix the delete command #51

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .github/dependabot.yml
Empty file.
92 changes: 92 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Release
#
on:
push:
#branches:
# - master
tags:
- 'v*.*.*'
#
jobs:
release:
runs-on: ubuntu-latest
#
steps:
- uses: actions/checkout@v3
#
- name: Setup Go environment
uses: actions/setup-go@v3
with:
go-version: '1.19'
- name: Install dependencies
run: go mod tidy

- name: Build go
run: go build .

- name: Get plugin metadata
id: metadata
run: |
sudo apt-get install jq
export NEXUS_ID=$(md5sum nexus-cli | sort | md5sum | cut -f1 -d ' ')
export NEXUS_VERSION=$(md5sum nexus-cli | sort | md5sum | cut -f1 -d ' ')
export NEXUS_PLUGIN_ARTIFACT=nexus-cli-${NEXUS_VERSION}.zip
export NEXUS_ARTIFACTS_CHECKSUM=${NEXUS_VERSION}.md5
echo "::set-output name=plugin-id::nexus-cli"
echo "::set-output name=plugin-version::${NEXUS_VERSION}"
echo "::set-output name=archive::${NEXUS_PLUGIN_ARTIFACT}"
echo "::set-output name=archive-checksum::${NEXUS_ARTIFACTS_CHECKSUM}"
echo ::set-output name=github-tag::${GITHUB_REF#refs/*/}
#
- name: Read changelog
id: changelog
run: |
awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > release_notes.md
echo "::set-output name=path::release_notes.md"

- name: Package plugin
id: package-plugin
run: |
zip ${{ steps.metadata.outputs.archive }} nexus-cli -r
md5sum ${{ steps.metadata.outputs.archive }} > ${{ steps.metadata.outputs.archive-checksum }}
echo "::set-output name=checksum::$(cat ./${{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)"

- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body_path: ${{ steps.changelog.outputs.path }}
draft: true
#
- name: Add plugin to release
id: upload-plugin-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ steps.metadata.outputs.archive }}
asset_name: ${{ steps.metadata.outputs.archive }}
asset_content_type: application/zip
#
- name: Add checksum to release
id: upload-checksum-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ steps.metadata.outputs.archive-checksum }}
asset_name: ${{ steps.metadata.outputs.archive-checksum }}
asset_content_type: text/plain
#
- name: Publish to Release
run: |
echo A draft release has been created for your plugin. Please review and publish it.
echo
echo '{ "id": "${{ steps.metadata.outputs.plugin-id }}", "type": "${{ steps.metadata.outputs.plugin-type }}", "url": "https://github.com/${{ github.repository }}", "versions": [ { "version": "${{ steps.metadata.outputs.plugin-version }}", "commit": "${{ github.sha }}", "url": "https://github.com/${{ github.repository }}", "download": { "any": { "url": "https://github.com/${{ github.repository }}/releases/download/v${{ steps.metadata.outputs.plugin-version }}/${{ steps.metadata.outputs.archive }}", "md5": "${{ steps.package-plugin.outputs.checksum }}" } } } ] }' | jq
#
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 1.0.0

Nexus CLI
18 changes: 18 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/Allan-Nava/nexus-cli

go 1.19

require (
github.com/BurntSushi/toml v1.2.0
github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be
github.com/urfave/cli v1.22.10
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)
23 changes: 23 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be h1:5JIRQAv1vxCnmi/YThMfd7Wpr8Sz3gju9wUUZmh23fA=
github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be/go.mod h1:pmsbmSTdgWwFXXRGWoSScdYQRTEOiymG2DTVN1/FWMQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
52 changes: 35 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"fmt"
"html/template"
"os"
"sort"

"github.com/mlabouardy/nexus-cli/registry"
"github.com/Allan-Nava/nexus-cli/registry"
"github.com/urfave/cli"
)

Expand All @@ -21,12 +22,16 @@ func main() {
app := cli.NewApp()
app.Name = "Nexus CLI"
app.Usage = "Manage Docker Private Registry on Nexus"
app.Version = "1.0.0-beta"
app.Version = "1.0.01"
app.Authors = []cli.Author{
cli.Author{
{
Name: "Mohamed Labouardy",
Email: "mohamed@labouardy.com",
},
{
Name: "Allan Nava",
Email: "allan.nava@hiway.media",
},
}
app.Commands = []cli.Command{
{
Expand Down Expand Up @@ -179,20 +184,24 @@ func listTagsByImage(c *cli.Context) error {
if imgName == "" {
cli.ShowSubcommandHelp(c)
}
var imageManifests []registry.ImageManifestV1
tags, err := r.ListTagsByImage(imgName)

compareStringNumber := func(str1, str2 string) bool {
return extractNumberFromString(str1) < extractNumberFromString(str2)
for _, tag := range tags {
manifest, _ := r.ImageManifestV1(imgName, tag)
imageManifests = append(imageManifests, manifest)
}
Compare(compareStringNumber).Sort(tags)
//
sort.Slice(imageManifests, func(i, j int) bool {
return imageManifests[i].Date.After(imageManifests[j].Date)
})

if err != nil {
return cli.NewExitError(err.Error(), 1)
}
for _, tag := range tags {
fmt.Println(tag)
for _, image := range imageManifests {
fmt.Println(image.Tag, " created: ", image.Created)
}
fmt.Printf("There are %d images for %s\n", len(tags), imgName)
fmt.Printf("There are %d images for %s\n", len(imageManifests), imgName)
return nil
}

Expand Down Expand Up @@ -237,20 +246,29 @@ func deleteImage(c *cli.Context) error {
cli.ShowSubcommandHelp(c)
} else {
tags, err := r.ListTagsByImage(imgName)
compareStringNumber := func(str1, str2 string) bool {
/*compareStringNumber := func(str1, str2 string) bool {
return extractNumberFromString(str1) < extractNumberFromString(str2)
}
Compare(compareStringNumber).Sort(tags)
Compare(compareStringNumber).Sort(tags)*/
var imageManifests []registry.ImageManifestV1
for _, tag := range tags {
manifest, _ := r.ImageManifestV1(imgName, tag)
imageManifests = append(imageManifests, manifest)
}
//
sort.Slice(imageManifests, func(i, j int) bool {
return imageManifests[i].Date.Before(imageManifests[j].Date)
})
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
if len(tags) >= keep {
for _, tag := range tags[:len(tags)-keep] {
fmt.Printf("%s:%s image will be deleted ...\n", imgName, tag)
r.DeleteImageByTag(imgName, tag)
if len(imageManifests) >= keep {
for _, tag := range imageManifests[:len(imageManifests)-keep] {
fmt.Printf("%s:%s image will be deleted date: %s...\n", imgName, tag.Tag, tag.Created)
r.DeleteImageByTag(imgName, tag.Tag)
}
} else {
fmt.Printf("Only %d images are available\n", len(tags))
fmt.Printf("Only %d images are available\n", len(imageManifests))
}
}
} else {
Expand Down
107 changes: 105 additions & 2 deletions registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/BurntSushi/toml"
"io"
"log"
"net/http"
"os"
"time"

"github.com/BurntSushi/toml"
"github.com/tidwall/gjson"
)


var Start = time.Now()
var elapsed = time.Since(Start)
const ACCEPT_HEADER_V1 = "application/vnd.docker.distribution.manifest.v1+json"
const ACCEPT_HEADER = "application/vnd.docker.distribution.manifest.v2+json"
const CREDENTIALS_FILE = ".credentials"

Expand Down Expand Up @@ -40,6 +49,15 @@ type LayerInfo struct {
Digest string `json:"digest"`
}

type ImageManifestV1 struct {
SchemaVersion int64 `json:"schemaVersion"`
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
Created string
Date time.Time
}

func NewRegistry() (Registry, error) {
r := Registry{}
if _, err := os.Stat(CREDENTIALS_FILE); os.IsNotExist(err) {
Expand Down Expand Up @@ -136,6 +154,91 @@ func (r Registry) ImageManifest(image string, tag string) (ImageManifest, error)

}

func (r Registry) ImageManifestV1(image string, tag string) (ImageManifestV1, error) {
var tr = &http.Transport{
MaxIdleConnsPerHost: 90,
}
var imageManifest ImageManifestV1
var client = &http.Client{
Transport: tr,
}
//elapsed = time.Since(Start)
// log.Printf("tag is %s", tag)
// log.Printf("begin get tag %s", elapsed)
// Start = time.Now()
url := fmt.Sprintf("%s/repository/%s/v2/%s/manifests/%s", r.Host, r.Repository, image, tag)
req, err := http.NewRequest("GET", url, nil)
req.Header = http.Header{
"Accept": {"application/vnd.docker.distribution.manifest.v2+json"},
}
if err != nil {
return imageManifest, err
}
req.SetBasicAuth(r.Username, r.Password)
req.Header.Add("Accept", ACCEPT_HEADER_V1)

resp, err := client.Do(req)
if err != nil {
return imageManifest, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return imageManifest, errors.New(fmt.Sprintf("HTTP Code: %d", resp.StatusCode))
}
b, err := io.ReadAll(resp.Body)
// b, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier
if err != nil {
log.Fatalln(err)
}
resp = nil
//json.NewDecoder(resp.Body).Decode(&imageManifest)
compatibilityString := gjson.GetBytes(b, `config`)
b = nil
digest := gjson.Get(compatibilityString.String(), `digest`)

// log.Printf("digest is %s", digest)

url = fmt.Sprintf("%s/repository/%s/v2/%s/blobs/%s", r.Host, r.Repository, image, digest)
req, err = http.NewRequest("GET", url, nil)
req.Header = http.Header{
"Accept": {"application/vnd.docker.distribution.manifest.v2+json"},
}
if err != nil {
return imageManifest, err
}
req.SetBasicAuth(r.Username, r.Password)
req.Header.Add("Accept", ACCEPT_HEADER_V1)

resp, err = client.Do(req)
if err != nil {
return imageManifest, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return imageManifest, errors.New(fmt.Sprintf("HTTP Code: %d", resp.StatusCode))
}
b, err = io.ReadAll(resp.Body)
// b, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier
if err != nil {
log.Fatalln(err)
}
resp = nil
//json.NewDecoder(resp.Body).Decode(&imageManifest)
created := gjson.Get(string(b),"created")
b = nil
if !created.Exists() {
return imageManifest, err
}

// log.Printf("created is %s", created.String())
imageManifest.Created = created.String()
imageManifest.Date = created.Time()
imageManifest.Tag = tag
imageManifest.Name = image
//
return imageManifest, nil
}

func (r Registry) DeleteImageByTag(image string, tag string) error {
sha, err := r.getImageSHA(image, tag)
if err != nil {
Expand Down Expand Up @@ -188,4 +291,4 @@ func (r Registry) getImageSHA(image string, tag string) (string, error) {
}

return resp.Header.Get("docker-content-digest"), nil
}
}