{"id":15747726,"url":"https://github.com/developer-guy/manipulate-docker-image-layers-with-crane","last_synced_at":"2025-03-31T06:41:39.339Z","repository":{"id":104879699,"uuid":"337768290","full_name":"developer-guy/manipulate-docker-image-layers-with-crane","owner":"developer-guy","description":"Inspired by @ahmetb's blog post: https://ahmet.im/blog/building-container-images-in-go/","archived":false,"fork":false,"pushed_at":"2021-02-13T16:29:30.000Z","size":34,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-05-01T16:29:42.839Z","etag":null,"topics":["container-image","container-image-layer","crane","dockerless","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/developer-guy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-10T15:35:06.000Z","updated_at":"2024-06-19T10:25:51.035Z","dependencies_parsed_at":"2023-11-30T03:31:03.621Z","dependency_job_id":null,"html_url":"https://github.com/developer-guy/manipulate-docker-image-layers-with-crane","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer-guy%2Fmanipulate-docker-image-layers-with-crane","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer-guy%2Fmanipulate-docker-image-layers-with-crane/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer-guy%2Fmanipulate-docker-image-layers-with-crane/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer-guy%2Fmanipulate-docker-image-layers-with-crane/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/developer-guy","download_url":"https://codeload.github.com/developer-guy/manipulate-docker-image-layers-with-crane/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246429449,"owners_count":20775805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["container-image","container-image-layer","crane","dockerless","golang"],"created_at":"2024-10-04T05:21:37.301Z","updated_at":"2025-03-31T06:41:39.317Z","avatar_url":"https://github.com/developer-guy.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e Credit: Inspired by @ahmetb's [latest blog post about building container images in Go.](ahmet.im/blog/building-container-images-in-go), please read this post before you move on to the hands-on section.\n\n# What is a crane tool?\n\n\u003cimg src=\"https://github.com/google/go-containerregistry/raw/main/images/crane.png\" height=\"300\"/\u003e\n\nGoogle has a repository called [\"go-containerregistry\"](https://github.com/google/go-containerregistry) which provides Go library and CLIs for working with container registries, [crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md) is one of them. More technically, the crane is a tool for interacting with remote images and registries.\n\n# Hands On\n\nLet's start with explaining the demo, first, we have a directory that includes a basic Go application that prints the content of the file to stdout, we'll start with building the container image, then with crane, we'll add a new layer to it using hello-world.txt that is available in the [layer/](./layer) directory, by doing so, we'll update the content of the file that is available within the container image.\n\nLet's take a look at the Dockerfile of the project.\n```Dockerfile\nFROM golang:1.15.7-alpine\n\nWORKDIR /app\n\nCOPY ./ ./\n\nENTRYPOINT [\"go\", \"run\", \"main.go\"]\n```\n\nit's very straightforward, then take a look at the go code.\n```golang\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n)\n\nfunc main() {\n\tcontent, err := ioutil.ReadFile(\"hello-world.txt\")\n\n\tif err != nil {\n\t\tlog.Fatal(\"could not read file, error:\", err)\n\t}\n\n\tlog.Println(\"Content of the file is : \", string(content))\n}\n```\n\nit's very straightforward too.\n\nLet's build the container image, in that case, we need docker to build the container image.\n```bash\n$ docker image build -t devopps/read-file-and-write-to-sdout:latest .\n[+] Building 7.3s (9/9) FINISHED\n =\u003e [internal] load build definition from Dockerfile                                                                                                                                                      0.0s\n =\u003e =\u003e transferring dockerfile: 131B                                                                                                                                                                      0.0s\n =\u003e [internal] load .dockerignore                                                                                                                                                                         0.0s\n =\u003e =\u003e transferring context: 2B                                                                                                                                                                           0.0s\n =\u003e [internal] load metadata for docker.io/library/golang:1.15.7-alpine                                                                                                                                   1.7s\n =\u003e [auth] library/golang:pull token for registry-1.docker.io                                                                                                                                             0.0s\n =\u003e [internal] load build context                                                                                                                                                                         0.0s\n =\u003e =\u003e transferring context: 463B                                                                                                                                                                         0.0s\n =\u003e [1/3] FROM docker.io/library/golang:1.15.7-alpine@sha256:dbda4e47937a3abb515c386d955002be5116d060c90d936127cc24ac439c815c                                                                             4.9s\n =\u003e =\u003e resolve docker.io/library/golang:1.15.7-alpine@sha256:dbda4e47937a3abb515c386d955002be5116d060c90d936127cc24ac439c815c                                                                             0.0s\n =\u003e =\u003e extracting sha256:4c0d98bf9879488e0407f897d9dd4bf758555a78e39675e72b5124ccf12c2580                                                                                                                 0.2s\n =\u003e =\u003e sha256:8b36f00a8e74ce31a867744519cc5db8c4aaeb181cffcda1b4d8269b1cc7f336 106.77MB / 106.77MB                                                                                                        0.0s\n =\u003e =\u003e sha256:5e5ebcc3e85238e4fbf5ab2428f9ed61dcede6c59b605d56b2f02fb991c70850 126B / 126B                                                                                                                0.0s\n =\u003e =\u003e sha256:dbda4e47937a3abb515c386d955002be5116d060c90d936127cc24ac439c815c 1.65kB / 1.65kB                                                                                                            0.0s\n =\u003e =\u003e sha256:18100456495c42bcdccab3411d8cfd56f3fdaa8527dd2b5a83800f96c7074a41 1.36kB / 1.36kB                                                                                                            0.0s\n =\u003e =\u003e sha256:54d042506068c9699d4236315fa76ea8789415c1079bcaff35fb3730ea649547 4.61kB / 4.61kB                                                                                                            0.0s\n =\u003e =\u003e sha256:4c0d98bf9879488e0407f897d9dd4bf758555a78e39675e72b5124ccf12c2580 2.81MB / 2.81MB                                                                                                            0.0s\n =\u003e =\u003e sha256:9e181322f1e7b3ebee5deeef0af7d13619801172e91d2d73dcf79b5d53d82d91 281.20kB / 281.20kB                                                                                                        0.0s\n =\u003e =\u003e sha256:6422294da7d35128e72551ecf15f3a4d9577e5cfa516b6d62fe8b841a9470cb3 154B / 154B                                                                                                                0.0s\n =\u003e =\u003e extracting sha256:9e181322f1e7b3ebee5deeef0af7d13619801172e91d2d73dcf79b5d53d82d91                                                                                                                 0.1s\n =\u003e =\u003e extracting sha256:6422294da7d35128e72551ecf15f3a4d9577e5cfa516b6d62fe8b841a9470cb3                                                                                                                 0.0s\n =\u003e =\u003e extracting sha256:8b36f00a8e74ce31a867744519cc5db8c4aaeb181cffcda1b4d8269b1cc7f336                                                                                                                 4.2s\n =\u003e =\u003e extracting sha256:5e5ebcc3e85238e4fbf5ab2428f9ed61dcede6c59b605d56b2f02fb991c70850                                                                                                                 0.0s\n =\u003e [2/3] WORKDIR /app                                                                                                                                                                                    0.5s\n =\u003e [3/3] COPY ./ ./                                                                                                                                                                                      0.0s\n =\u003e exporting to image                                                                                                                                                                                    0.0s\n =\u003e =\u003e exporting layers                                                                                                                                                                                   0.0s\n =\u003e =\u003e writing image sha256:ac1ec869614296ba300d64189d6706865396fd9c45caefbb3a9a614dfa1cdd81                                                                                                              0.0s\n =\u003e =\u003e naming to docker.io/devopps/read-file-and-write-to-sdout:latest                                                                                                                                    0.0s\n```\n\nRun it and verify the output because it should match with the [hello-world.txt](./read-file-and-write-to-sdout/hello-world.txt).\n```bash\n$ docker container run devopps/read-file-and-write-to-sdout:latest\n2021/02/13 15:12:42 Content of the file is:  hello world\n```\n\nLet's edit this image with the crane by adding a new layer to it, the layer that we are going to add is the same file but with different content. So, if we add the file to the workdir of the image by crane, this code will be going to start to use the file that we add with the layer.\n\nThis is the following [content](./layer/hello-world.txt) that we'll add as a final layer of the image.\n```text\nhello world made by crane\n```\n\nLet's take a look at the code.\n```golang\npackage main\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/google/go-containerregistry/pkg/crane\"\n\t\"github.com/google/go-containerregistry/pkg/name\"\n\t\"github.com/google/go-containerregistry/pkg/v1/daemon\"\n\t\"github.com/google/go-containerregistry/pkg/v1/mutate\"\n\t\"github.com/google/go-containerregistry/pkg/v1/tarball\"\n)\n\nfunc main() {\n\timg, err := crane.Pull(\"devopps/read-file-and-write-to-sdout:latest\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar b bytes.Buffer\n\ttw := tar.NewWriter(\u0026b)\n\terr = addFileToTarWriter(\"/Users/batuhan.apaydin/workspace/projects/personal/poc/manipulate-docker-image-layers-with-crane/layer\",\n\t\t\"/app\",\n\t\t\"/Users/batuhan.apaydin/workspace/projects/personal/poc/manipulate-docker-image-layers-with-crane/layer/hello-world.txt\", tw)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\taddLayer, err := tarball.LayerFromReader(\u0026b)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tnewImg, err := mutate.AppendLayers(img, addLayer)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttag, err := name.NewTag(\"devopps/read-file-and-write-to-sdout:foo\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n    // uncomment these lines if you want to save this image to the local image cache - \"NEEDS Dockerin this case\"\n\t//if s, err := daemon.Write(tag, newImg); err != nil {\n\t//\tpanic(err)\n\t//} else {\n\t//\tfmt.Println(s)\n\t//}\n\n     // push to remote registry\n\tif err := crane.Push(newImg, tag.String()); err != nil {\n\t\tpanic(err)\n\t}\n\t\n\tlog.Printf(\"image %s pushed to the registry succesfully\\n\", tag.String())\n}\n\nfunc addFileToTarWriter(root, targetPath, filePath string, tarWriter *tar.Writer) error {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"Could not open file '%s', got error '%s'\", filePath, err.Error()))\n\t}\n\tdefer file.Close()\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"Could not get stat for file '%s', got error '%s'\", filePath, err.Error()))\n\t}\n\n\trel, err := filepath.Rel(root, filePath)\n\n\theader := \u0026tar.Header{\n\t\tName:    path.Join(targetPath, filepath.ToSlash(rel)),\n\t\tSize:    stat.Size(),\n\t\tMode:    int64(stat.Mode()),\n\t\tModTime: stat.ModTime(),\n\t}\n\n\terr = tarWriter.WriteHeader(header)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"Could not write header for file '%s', got error '%s'\", filePath, err.Error()))\n\t}\n\n\t_, err = io.Copy(tarWriter, file)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"Could not copy the file '%s' data to the tarball, got error '%s'\", filePath, err.Error()))\n\t}\n\n\treturn nil\n}\n\n```\n\n\u003e IMPORTANT: Before running this code, please shutdown the Docker.\n\nYou should notice that, first, we pull the image then we create a layer as tar format that includes my hello-world.txt then we append the new layer to the image.\n\nLets run this code.\n```bash\n$ go run -v ./main.go\n2021/02/13 18:42:28 image devopps/read-file-and-write-to-sdout:foo pushed to the registry succesfully\n```\n\n\u003e IMPORTANT: start the Docker again.\n\nLets verify the output of the container from the edited image.\n```bash\n$ docker container run devopps/read-file-and-write-to-sdout:foo\n2021/02/13 16:29:09 Content of the file is :  hello world made by crane\n```\n\nTada, it worked.🎉🎉🎉🎉.\n\n\n\u003e BONUS: crane also can be installed as a cli, go to the [installation page](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md#installation) and download it.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloper-guy%2Fmanipulate-docker-image-layers-with-crane","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeveloper-guy%2Fmanipulate-docker-image-layers-with-crane","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloper-guy%2Fmanipulate-docker-image-layers-with-crane/lists"}