Repository: alexellis/k3sup
Branch: master
Commit: 0e4426970b9f
Files: 911
Total size: 10.7 MB
Directory structure:
gitextract_pskgmy49/
├── .DEREK.yml
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── report-an-issue.md
│ │ └── request-a-feature.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── build.yaml
│ └── publish.yaml
├── .gitignore
├── EULA.md
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│ ├── get-config.go
│ ├── get.go
│ ├── get_pro.go
│ ├── install.go
│ ├── install_test.go
│ ├── join.go
│ ├── join_test.go
│ ├── node-token.go
│ ├── plan.go
│ ├── pro.go
│ ├── ready.go
│ ├── update.go
│ └── version.go
├── docs/
│ └── assets/
│ └── README.md
├── get.sh
├── go.mod
├── go.sum
├── hack/
│ ├── hashgen.sh
│ └── platform-tag.sh
├── main.go
├── pkg/
│ ├── operator/
│ │ ├── exec_operator.go
│ │ ├── operator.go
│ │ └── ssh_operator.go
│ └── thanks.go
└── vendor/
├── github.com/
│ ├── alexellis/
│ │ ├── arkade/
│ │ │ ├── LICENSE
│ │ │ └── pkg/
│ │ │ ├── archive/
│ │ │ │ ├── untar.go
│ │ │ │ ├── untar_nested.go
│ │ │ │ └── unzip.go
│ │ │ └── env/
│ │ │ └── env.go
│ │ └── go-execute/
│ │ └── v2/
│ │ ├── .DEREK.yml
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── Makefile
│ │ ├── README.md
│ │ └── exec.go
│ ├── containerd/
│ │ └── stargz-snapshotter/
│ │ └── estargz/
│ │ ├── LICENSE
│ │ ├── build.go
│ │ ├── errorutil/
│ │ │ └── errors.go
│ │ ├── estargz.go
│ │ ├── gzip.go
│ │ ├── testutil.go
│ │ └── types.go
│ ├── docker/
│ │ ├── cli/
│ │ │ ├── AUTHORS
│ │ │ ├── LICENSE
│ │ │ ├── NOTICE
│ │ │ └── cli/
│ │ │ └── config/
│ │ │ ├── config.go
│ │ │ ├── configfile/
│ │ │ │ ├── file.go
│ │ │ │ ├── file_unix.go
│ │ │ │ └── file_windows.go
│ │ │ ├── credentials/
│ │ │ │ ├── credentials.go
│ │ │ │ ├── default_store.go
│ │ │ │ ├── default_store_darwin.go
│ │ │ │ ├── default_store_linux.go
│ │ │ │ ├── default_store_unsupported.go
│ │ │ │ ├── default_store_windows.go
│ │ │ │ ├── file_store.go
│ │ │ │ └── native_store.go
│ │ │ ├── memorystore/
│ │ │ │ └── store.go
│ │ │ └── types/
│ │ │ └── authconfig.go
│ │ ├── distribution/
│ │ │ ├── LICENSE
│ │ │ └── registry/
│ │ │ └── client/
│ │ │ └── auth/
│ │ │ └── challenge/
│ │ │ ├── addr.go
│ │ │ └── authchallenge.go
│ │ └── docker-credential-helpers/
│ │ ├── LICENSE
│ │ ├── client/
│ │ │ ├── client.go
│ │ │ └── command.go
│ │ └── credentials/
│ │ ├── credentials.go
│ │ ├── error.go
│ │ ├── helper.go
│ │ └── version.go
│ ├── google/
│ │ └── go-containerregistry/
│ │ ├── LICENSE
│ │ ├── internal/
│ │ │ ├── and/
│ │ │ │ └── and_closer.go
│ │ │ ├── compression/
│ │ │ │ └── compression.go
│ │ │ ├── estargz/
│ │ │ │ └── estargz.go
│ │ │ ├── gzip/
│ │ │ │ └── zip.go
│ │ │ ├── redact/
│ │ │ │ └── redact.go
│ │ │ ├── retry/
│ │ │ │ ├── retry.go
│ │ │ │ └── wait/
│ │ │ │ └── kubernetes_apimachinery_wait.go
│ │ │ ├── verify/
│ │ │ │ └── verify.go
│ │ │ ├── windows/
│ │ │ │ └── windows.go
│ │ │ └── zstd/
│ │ │ └── zstd.go
│ │ └── pkg/
│ │ ├── authn/
│ │ │ ├── README.md
│ │ │ ├── anon.go
│ │ │ ├── auth.go
│ │ │ ├── authn.go
│ │ │ ├── basic.go
│ │ │ ├── bearer.go
│ │ │ ├── doc.go
│ │ │ ├── keychain.go
│ │ │ └── multikeychain.go
│ │ ├── compression/
│ │ │ └── compression.go
│ │ ├── crane/
│ │ │ ├── append.go
│ │ │ ├── catalog.go
│ │ │ ├── config.go
│ │ │ ├── copy.go
│ │ │ ├── delete.go
│ │ │ ├── digest.go
│ │ │ ├── doc.go
│ │ │ ├── export.go
│ │ │ ├── filemap.go
│ │ │ ├── get.go
│ │ │ ├── list.go
│ │ │ ├── manifest.go
│ │ │ ├── options.go
│ │ │ ├── pull.go
│ │ │ ├── push.go
│ │ │ └── tag.go
│ │ ├── legacy/
│ │ │ ├── config.go
│ │ │ ├── doc.go
│ │ │ └── tarball/
│ │ │ ├── README.md
│ │ │ ├── doc.go
│ │ │ └── write.go
│ │ ├── logs/
│ │ │ └── logs.go
│ │ ├── name/
│ │ │ ├── README.md
│ │ │ ├── check.go
│ │ │ ├── digest.go
│ │ │ ├── doc.go
│ │ │ ├── errors.go
│ │ │ ├── options.go
│ │ │ ├── ref.go
│ │ │ ├── registry.go
│ │ │ ├── repository.go
│ │ │ └── tag.go
│ │ └── v1/
│ │ ├── config.go
│ │ ├── doc.go
│ │ ├── empty/
│ │ │ ├── README.md
│ │ │ ├── doc.go
│ │ │ ├── image.go
│ │ │ └── index.go
│ │ ├── hash.go
│ │ ├── image.go
│ │ ├── index.go
│ │ ├── layer.go
│ │ ├── layout/
│ │ │ ├── README.md
│ │ │ ├── blob.go
│ │ │ ├── doc.go
│ │ │ ├── gc.go
│ │ │ ├── image.go
│ │ │ ├── index.go
│ │ │ ├── layoutpath.go
│ │ │ ├── options.go
│ │ │ ├── read.go
│ │ │ └── write.go
│ │ ├── manifest.go
│ │ ├── match/
│ │ │ └── match.go
│ │ ├── mutate/
│ │ │ ├── README.md
│ │ │ ├── doc.go
│ │ │ ├── image.go
│ │ │ ├── index.go
│ │ │ ├── mutate.go
│ │ │ └── rebase.go
│ │ ├── partial/
│ │ │ ├── README.md
│ │ │ ├── compressed.go
│ │ │ ├── doc.go
│ │ │ ├── image.go
│ │ │ ├── index.go
│ │ │ ├── uncompressed.go
│ │ │ └── with.go
│ │ ├── platform.go
│ │ ├── progress.go
│ │ ├── remote/
│ │ │ ├── README.md
│ │ │ ├── catalog.go
│ │ │ ├── check.go
│ │ │ ├── delete.go
│ │ │ ├── descriptor.go
│ │ │ ├── doc.go
│ │ │ ├── fetcher.go
│ │ │ ├── image.go
│ │ │ ├── index.go
│ │ │ ├── layer.go
│ │ │ ├── list.go
│ │ │ ├── mount.go
│ │ │ ├── multi_write.go
│ │ │ ├── options.go
│ │ │ ├── progress.go
│ │ │ ├── puller.go
│ │ │ ├── pusher.go
│ │ │ ├── referrers.go
│ │ │ ├── schema1.go
│ │ │ ├── transport/
│ │ │ │ ├── README.md
│ │ │ │ ├── basic.go
│ │ │ │ ├── bearer.go
│ │ │ │ ├── doc.go
│ │ │ │ ├── error.go
│ │ │ │ ├── logger.go
│ │ │ │ ├── ping.go
│ │ │ │ ├── retry.go
│ │ │ │ ├── schemer.go
│ │ │ │ ├── scope.go
│ │ │ │ ├── transport.go
│ │ │ │ └── useragent.go
│ │ │ └── write.go
│ │ ├── stream/
│ │ │ ├── README.md
│ │ │ └── layer.go
│ │ ├── tarball/
│ │ │ ├── README.md
│ │ │ ├── doc.go
│ │ │ ├── image.go
│ │ │ ├── layer.go
│ │ │ └── write.go
│ │ ├── types/
│ │ │ └── types.go
│ │ └── zz_deepcopy_generated.go
│ ├── inconshreveable/
│ │ └── mousetrap/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── trap_others.go
│ │ └── trap_windows.go
│ ├── klauspost/
│ │ └── compress/
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── .goreleaser.yml
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── SECURITY.md
│ │ ├── compressible.go
│ │ ├── fse/
│ │ │ ├── README.md
│ │ │ ├── bitreader.go
│ │ │ ├── bitwriter.go
│ │ │ ├── bytereader.go
│ │ │ ├── compress.go
│ │ │ ├── decompress.go
│ │ │ └── fse.go
│ │ ├── gen.sh
│ │ ├── huff0/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── bitreader.go
│ │ │ ├── bitwriter.go
│ │ │ ├── compress.go
│ │ │ ├── decompress.go
│ │ │ ├── decompress_amd64.go
│ │ │ ├── decompress_amd64.s
│ │ │ ├── decompress_generic.go
│ │ │ └── huff0.go
│ │ ├── internal/
│ │ │ ├── cpuinfo/
│ │ │ │ ├── cpuinfo.go
│ │ │ │ ├── cpuinfo_amd64.go
│ │ │ │ └── cpuinfo_amd64.s
│ │ │ ├── le/
│ │ │ │ ├── le.go
│ │ │ │ ├── unsafe_disabled.go
│ │ │ │ └── unsafe_enabled.go
│ │ │ └── snapref/
│ │ │ ├── LICENSE
│ │ │ ├── decode.go
│ │ │ ├── decode_other.go
│ │ │ ├── encode.go
│ │ │ ├── encode_other.go
│ │ │ └── snappy.go
│ │ ├── s2sx.mod
│ │ ├── s2sx.sum
│ │ └── zstd/
│ │ ├── README.md
│ │ ├── bitreader.go
│ │ ├── bitwriter.go
│ │ ├── blockdec.go
│ │ ├── blockenc.go
│ │ ├── blocktype_string.go
│ │ ├── bytebuf.go
│ │ ├── bytereader.go
│ │ ├── decodeheader.go
│ │ ├── decoder.go
│ │ ├── decoder_options.go
│ │ ├── dict.go
│ │ ├── enc_base.go
│ │ ├── enc_best.go
│ │ ├── enc_better.go
│ │ ├── enc_dfast.go
│ │ ├── enc_fast.go
│ │ ├── encoder.go
│ │ ├── encoder_options.go
│ │ ├── framedec.go
│ │ ├── frameenc.go
│ │ ├── fse_decoder.go
│ │ ├── fse_decoder_amd64.go
│ │ ├── fse_decoder_amd64.s
│ │ ├── fse_decoder_generic.go
│ │ ├── fse_encoder.go
│ │ ├── fse_predefined.go
│ │ ├── hash.go
│ │ ├── history.go
│ │ ├── internal/
│ │ │ └── xxhash/
│ │ │ ├── README.md
│ │ │ ├── xxhash.go
│ │ │ ├── xxhash_amd64.s
│ │ │ ├── xxhash_arm64.s
│ │ │ ├── xxhash_asm.go
│ │ │ ├── xxhash_other.go
│ │ │ └── xxhash_safe.go
│ │ ├── matchlen_amd64.go
│ │ ├── matchlen_amd64.s
│ │ ├── matchlen_generic.go
│ │ ├── seqdec.go
│ │ ├── seqdec_amd64.go
│ │ ├── seqdec_amd64.s
│ │ ├── seqdec_generic.go
│ │ ├── seqenc.go
│ │ ├── simple_go124.go
│ │ ├── snappy.go
│ │ ├── zip.go
│ │ └── zstd.go
│ ├── mitchellh/
│ │ └── go-homedir/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── homedir.go
│ ├── morikuni/
│ │ └── aec/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── aec.go
│ │ ├── ansi.go
│ │ ├── builder.go
│ │ └── sgr.go
│ ├── opencontainers/
│ │ ├── go-digest/
│ │ │ ├── .mailmap
│ │ │ ├── .pullapprove.yml
│ │ │ ├── .travis.yml
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── LICENSE
│ │ │ ├── LICENSE.docs
│ │ │ ├── MAINTAINERS
│ │ │ ├── README.md
│ │ │ ├── algorithm.go
│ │ │ ├── digest.go
│ │ │ ├── digester.go
│ │ │ ├── doc.go
│ │ │ └── verifiers.go
│ │ └── image-spec/
│ │ ├── LICENSE
│ │ └── specs-go/
│ │ ├── v1/
│ │ │ ├── annotations.go
│ │ │ ├── config.go
│ │ │ ├── descriptor.go
│ │ │ ├── index.go
│ │ │ ├── layout.go
│ │ │ ├── manifest.go
│ │ │ └── mediatype.go
│ │ ├── version.go
│ │ └── versioned.go
│ ├── spf13/
│ │ ├── cobra/
│ │ │ ├── .gitignore
│ │ │ ├── .golangci.yml
│ │ │ ├── .mailmap
│ │ │ ├── CONDUCT.md
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── MAINTAINERS
│ │ │ ├── Makefile
│ │ │ ├── README.md
│ │ │ ├── SECURITY.md
│ │ │ ├── active_help.go
│ │ │ ├── args.go
│ │ │ ├── bash_completions.go
│ │ │ ├── bash_completionsV2.go
│ │ │ ├── cobra.go
│ │ │ ├── command.go
│ │ │ ├── command_notwin.go
│ │ │ ├── command_win.go
│ │ │ ├── completions.go
│ │ │ ├── fish_completions.go
│ │ │ ├── flag_groups.go
│ │ │ ├── powershell_completions.go
│ │ │ ├── shell_completions.go
│ │ │ └── zsh_completions.go
│ │ └── pflag/
│ │ ├── .editorconfig
│ │ ├── .gitignore
│ │ ├── .golangci.yaml
│ │ ├── .travis.yml
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── bool.go
│ │ ├── bool_func.go
│ │ ├── bool_slice.go
│ │ ├── bytes.go
│ │ ├── count.go
│ │ ├── duration.go
│ │ ├── duration_slice.go
│ │ ├── errors.go
│ │ ├── flag.go
│ │ ├── float32.go
│ │ ├── float32_slice.go
│ │ ├── float64.go
│ │ ├── float64_slice.go
│ │ ├── func.go
│ │ ├── golangflag.go
│ │ ├── int.go
│ │ ├── int16.go
│ │ ├── int32.go
│ │ ├── int32_slice.go
│ │ ├── int64.go
│ │ ├── int64_slice.go
│ │ ├── int8.go
│ │ ├── int_slice.go
│ │ ├── ip.go
│ │ ├── ip_slice.go
│ │ ├── ipmask.go
│ │ ├── ipnet.go
│ │ ├── ipnet_slice.go
│ │ ├── string.go
│ │ ├── string_array.go
│ │ ├── string_slice.go
│ │ ├── string_to_int.go
│ │ ├── string_to_int64.go
│ │ ├── string_to_string.go
│ │ ├── text.go
│ │ ├── time.go
│ │ ├── uint.go
│ │ ├── uint16.go
│ │ ├── uint32.go
│ │ ├── uint64.go
│ │ ├── uint8.go
│ │ └── uint_slice.go
│ └── vbatts/
│ └── tar-split/
│ ├── LICENSE
│ └── archive/
│ └── tar/
│ ├── common.go
│ ├── format.go
│ ├── reader.go
│ ├── stat_actime1.go
│ ├── stat_actime2.go
│ ├── stat_unix.go
│ ├── strconv.go
│ └── writer.go
└── golang.org/
└── x/
├── crypto/
│ ├── LICENSE
│ ├── PATENTS
│ ├── blowfish/
│ │ ├── block.go
│ │ ├── cipher.go
│ │ └── const.go
│ ├── chacha20/
│ │ ├── chacha_arm64.go
│ │ ├── chacha_arm64.s
│ │ ├── chacha_generic.go
│ │ ├── chacha_noasm.go
│ │ ├── chacha_ppc64x.go
│ │ ├── chacha_ppc64x.s
│ │ ├── chacha_s390x.go
│ │ ├── chacha_s390x.s
│ │ └── xor.go
│ ├── curve25519/
│ │ └── curve25519.go
│ ├── internal/
│ │ ├── alias/
│ │ │ ├── alias.go
│ │ │ └── alias_purego.go
│ │ └── poly1305/
│ │ ├── mac_noasm.go
│ │ ├── poly1305.go
│ │ ├── sum_amd64.s
│ │ ├── sum_asm.go
│ │ ├── sum_generic.go
│ │ ├── sum_loong64.s
│ │ ├── sum_ppc64x.s
│ │ ├── sum_s390x.go
│ │ └── sum_s390x.s
│ └── ssh/
│ ├── agent/
│ │ ├── client.go
│ │ ├── forward.go
│ │ ├── keyring.go
│ │ └── server.go
│ ├── buffer.go
│ ├── certs.go
│ ├── channel.go
│ ├── cipher.go
│ ├── client.go
│ ├── client_auth.go
│ ├── common.go
│ ├── connection.go
│ ├── doc.go
│ ├── handshake.go
│ ├── internal/
│ │ └── bcrypt_pbkdf/
│ │ └── bcrypt_pbkdf.go
│ ├── kex.go
│ ├── keys.go
│ ├── mac.go
│ ├── messages.go
│ ├── mlkem.go
│ ├── mux.go
│ ├── server.go
│ ├── session.go
│ ├── ssh_gss.go
│ ├── streamlocal.go
│ ├── tcpip.go
│ └── transport.go
├── sync/
│ ├── LICENSE
│ ├── PATENTS
│ └── errgroup/
│ └── errgroup.go
├── sys/
│ ├── LICENSE
│ ├── PATENTS
│ ├── cpu/
│ │ ├── asm_aix_ppc64.s
│ │ ├── asm_darwin_x86_gc.s
│ │ ├── byteorder.go
│ │ ├── cpu.go
│ │ ├── cpu_aix.go
│ │ ├── cpu_arm.go
│ │ ├── cpu_arm64.go
│ │ ├── cpu_arm64.s
│ │ ├── cpu_darwin_x86.go
│ │ ├── cpu_gc_arm64.go
│ │ ├── cpu_gc_s390x.go
│ │ ├── cpu_gc_x86.go
│ │ ├── cpu_gc_x86.s
│ │ ├── cpu_gccgo_arm64.go
│ │ ├── cpu_gccgo_s390x.go
│ │ ├── cpu_gccgo_x86.c
│ │ ├── cpu_gccgo_x86.go
│ │ ├── cpu_linux.go
│ │ ├── cpu_linux_arm.go
│ │ ├── cpu_linux_arm64.go
│ │ ├── cpu_linux_loong64.go
│ │ ├── cpu_linux_mips64x.go
│ │ ├── cpu_linux_noinit.go
│ │ ├── cpu_linux_ppc64x.go
│ │ ├── cpu_linux_riscv64.go
│ │ ├── cpu_linux_s390x.go
│ │ ├── cpu_loong64.go
│ │ ├── cpu_loong64.s
│ │ ├── cpu_mips64x.go
│ │ ├── cpu_mipsx.go
│ │ ├── cpu_netbsd_arm64.go
│ │ ├── cpu_openbsd_arm64.go
│ │ ├── cpu_openbsd_arm64.s
│ │ ├── cpu_other_arm.go
│ │ ├── cpu_other_arm64.go
│ │ ├── cpu_other_mips64x.go
│ │ ├── cpu_other_ppc64x.go
│ │ ├── cpu_other_riscv64.go
│ │ ├── cpu_other_x86.go
│ │ ├── cpu_ppc64x.go
│ │ ├── cpu_riscv64.go
│ │ ├── cpu_s390x.go
│ │ ├── cpu_s390x.s
│ │ ├── cpu_wasm.go
│ │ ├── cpu_windows_arm64.go
│ │ ├── cpu_x86.go
│ │ ├── cpu_zos.go
│ │ ├── cpu_zos_s390x.go
│ │ ├── endian_big.go
│ │ ├── endian_little.go
│ │ ├── hwcap_linux.go
│ │ ├── parse.go
│ │ ├── proc_cpuinfo_linux.go
│ │ ├── runtime_auxv.go
│ │ ├── runtime_auxv_go121.go
│ │ ├── syscall_aix_gccgo.go
│ │ ├── syscall_aix_ppc64_gc.go
│ │ └── syscall_darwin_x86_gc.go
│ ├── plan9/
│ │ ├── asm.s
│ │ ├── asm_plan9_386.s
│ │ ├── asm_plan9_amd64.s
│ │ ├── asm_plan9_arm.s
│ │ ├── const_plan9.go
│ │ ├── dir_plan9.go
│ │ ├── env_plan9.go
│ │ ├── errors_plan9.go
│ │ ├── mkall.sh
│ │ ├── mkerrors.sh
│ │ ├── mksysnum_plan9.sh
│ │ ├── pwd_plan9.go
│ │ ├── race.go
│ │ ├── race0.go
│ │ ├── str.go
│ │ ├── syscall.go
│ │ ├── syscall_plan9.go
│ │ ├── zsyscall_plan9_386.go
│ │ ├── zsyscall_plan9_amd64.go
│ │ ├── zsyscall_plan9_arm.go
│ │ └── zsysnum_plan9.go
│ ├── unix/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── affinity_linux.go
│ │ ├── aliases.go
│ │ ├── asm_aix_ppc64.s
│ │ ├── asm_bsd_386.s
│ │ ├── asm_bsd_amd64.s
│ │ ├── asm_bsd_arm.s
│ │ ├── asm_bsd_arm64.s
│ │ ├── asm_bsd_ppc64.s
│ │ ├── asm_bsd_riscv64.s
│ │ ├── asm_linux_386.s
│ │ ├── asm_linux_amd64.s
│ │ ├── asm_linux_arm.s
│ │ ├── asm_linux_arm64.s
│ │ ├── asm_linux_loong64.s
│ │ ├── asm_linux_mips64x.s
│ │ ├── asm_linux_mipsx.s
│ │ ├── asm_linux_ppc64x.s
│ │ ├── asm_linux_riscv64.s
│ │ ├── asm_linux_s390x.s
│ │ ├── asm_openbsd_mips64.s
│ │ ├── asm_solaris_amd64.s
│ │ ├── asm_zos_s390x.s
│ │ ├── auxv.go
│ │ ├── auxv_unsupported.go
│ │ ├── bluetooth_linux.go
│ │ ├── bpxsvc_zos.go
│ │ ├── bpxsvc_zos.s
│ │ ├── cap_freebsd.go
│ │ ├── constants.go
│ │ ├── dev_aix_ppc.go
│ │ ├── dev_aix_ppc64.go
│ │ ├── dev_darwin.go
│ │ ├── dev_dragonfly.go
│ │ ├── dev_freebsd.go
│ │ ├── dev_linux.go
│ │ ├── dev_netbsd.go
│ │ ├── dev_openbsd.go
│ │ ├── dev_zos.go
│ │ ├── dirent.go
│ │ ├── endian_big.go
│ │ ├── endian_little.go
│ │ ├── env_unix.go
│ │ ├── fcntl.go
│ │ ├── fcntl_darwin.go
│ │ ├── fcntl_linux_32bit.go
│ │ ├── fdset.go
│ │ ├── gccgo.go
│ │ ├── gccgo_c.c
│ │ ├── gccgo_linux_amd64.go
│ │ ├── ifreq_linux.go
│ │ ├── ioctl_linux.go
│ │ ├── ioctl_signed.go
│ │ ├── ioctl_unsigned.go
│ │ ├── ioctl_zos.go
│ │ ├── mkall.sh
│ │ ├── mkerrors.sh
│ │ ├── mmap_nomremap.go
│ │ ├── mremap.go
│ │ ├── pagesize_unix.go
│ │ ├── pledge_openbsd.go
│ │ ├── ptrace_darwin.go
│ │ ├── ptrace_ios.go
│ │ ├── race.go
│ │ ├── race0.go
│ │ ├── readdirent_getdents.go
│ │ ├── readdirent_getdirentries.go
│ │ ├── sockcmsg_dragonfly.go
│ │ ├── sockcmsg_linux.go
│ │ ├── sockcmsg_unix.go
│ │ ├── sockcmsg_unix_other.go
│ │ ├── sockcmsg_zos.go
│ │ ├── symaddr_zos_s390x.s
│ │ ├── syscall.go
│ │ ├── syscall_aix.go
│ │ ├── syscall_aix_ppc.go
│ │ ├── syscall_aix_ppc64.go
│ │ ├── syscall_bsd.go
│ │ ├── syscall_darwin.go
│ │ ├── syscall_darwin_amd64.go
│ │ ├── syscall_darwin_arm64.go
│ │ ├── syscall_darwin_libSystem.go
│ │ ├── syscall_dragonfly.go
│ │ ├── syscall_dragonfly_amd64.go
│ │ ├── syscall_freebsd.go
│ │ ├── syscall_freebsd_386.go
│ │ ├── syscall_freebsd_amd64.go
│ │ ├── syscall_freebsd_arm.go
│ │ ├── syscall_freebsd_arm64.go
│ │ ├── syscall_freebsd_riscv64.go
│ │ ├── syscall_hurd.go
│ │ ├── syscall_hurd_386.go
│ │ ├── syscall_illumos.go
│ │ ├── syscall_linux.go
│ │ ├── syscall_linux_386.go
│ │ ├── syscall_linux_alarm.go
│ │ ├── syscall_linux_amd64.go
│ │ ├── syscall_linux_amd64_gc.go
│ │ ├── syscall_linux_arm.go
│ │ ├── syscall_linux_arm64.go
│ │ ├── syscall_linux_gc.go
│ │ ├── syscall_linux_gc_386.go
│ │ ├── syscall_linux_gc_arm.go
│ │ ├── syscall_linux_gccgo_386.go
│ │ ├── syscall_linux_gccgo_arm.go
│ │ ├── syscall_linux_loong64.go
│ │ ├── syscall_linux_mips64x.go
│ │ ├── syscall_linux_mipsx.go
│ │ ├── syscall_linux_ppc.go
│ │ ├── syscall_linux_ppc64x.go
│ │ ├── syscall_linux_riscv64.go
│ │ ├── syscall_linux_s390x.go
│ │ ├── syscall_linux_sparc64.go
│ │ ├── syscall_netbsd.go
│ │ ├── syscall_netbsd_386.go
│ │ ├── syscall_netbsd_amd64.go
│ │ ├── syscall_netbsd_arm.go
│ │ ├── syscall_netbsd_arm64.go
│ │ ├── syscall_openbsd.go
│ │ ├── syscall_openbsd_386.go
│ │ ├── syscall_openbsd_amd64.go
│ │ ├── syscall_openbsd_arm.go
│ │ ├── syscall_openbsd_arm64.go
│ │ ├── syscall_openbsd_libc.go
│ │ ├── syscall_openbsd_mips64.go
│ │ ├── syscall_openbsd_ppc64.go
│ │ ├── syscall_openbsd_riscv64.go
│ │ ├── syscall_solaris.go
│ │ ├── syscall_solaris_amd64.go
│ │ ├── syscall_unix.go
│ │ ├── syscall_unix_gc.go
│ │ ├── syscall_unix_gc_ppc64x.go
│ │ ├── syscall_zos_s390x.go
│ │ ├── sysvshm_linux.go
│ │ ├── sysvshm_unix.go
│ │ ├── sysvshm_unix_other.go
│ │ ├── timestruct.go
│ │ ├── unveil_openbsd.go
│ │ ├── vgetrandom_linux.go
│ │ ├── vgetrandom_unsupported.go
│ │ ├── xattr_bsd.go
│ │ ├── zerrors_aix_ppc.go
│ │ ├── zerrors_aix_ppc64.go
│ │ ├── zerrors_darwin_amd64.go
│ │ ├── zerrors_darwin_arm64.go
│ │ ├── zerrors_dragonfly_amd64.go
│ │ ├── zerrors_freebsd_386.go
│ │ ├── zerrors_freebsd_amd64.go
│ │ ├── zerrors_freebsd_arm.go
│ │ ├── zerrors_freebsd_arm64.go
│ │ ├── zerrors_freebsd_riscv64.go
│ │ ├── zerrors_linux.go
│ │ ├── zerrors_linux_386.go
│ │ ├── zerrors_linux_amd64.go
│ │ ├── zerrors_linux_arm.go
│ │ ├── zerrors_linux_arm64.go
│ │ ├── zerrors_linux_loong64.go
│ │ ├── zerrors_linux_mips.go
│ │ ├── zerrors_linux_mips64.go
│ │ ├── zerrors_linux_mips64le.go
│ │ ├── zerrors_linux_mipsle.go
│ │ ├── zerrors_linux_ppc.go
│ │ ├── zerrors_linux_ppc64.go
│ │ ├── zerrors_linux_ppc64le.go
│ │ ├── zerrors_linux_riscv64.go
│ │ ├── zerrors_linux_s390x.go
│ │ ├── zerrors_linux_sparc64.go
│ │ ├── zerrors_netbsd_386.go
│ │ ├── zerrors_netbsd_amd64.go
│ │ ├── zerrors_netbsd_arm.go
│ │ ├── zerrors_netbsd_arm64.go
│ │ ├── zerrors_openbsd_386.go
│ │ ├── zerrors_openbsd_amd64.go
│ │ ├── zerrors_openbsd_arm.go
│ │ ├── zerrors_openbsd_arm64.go
│ │ ├── zerrors_openbsd_mips64.go
│ │ ├── zerrors_openbsd_ppc64.go
│ │ ├── zerrors_openbsd_riscv64.go
│ │ ├── zerrors_solaris_amd64.go
│ │ ├── zerrors_zos_s390x.go
│ │ ├── zptrace_armnn_linux.go
│ │ ├── zptrace_linux_arm64.go
│ │ ├── zptrace_mipsnn_linux.go
│ │ ├── zptrace_mipsnnle_linux.go
│ │ ├── zptrace_x86_linux.go
│ │ ├── zsymaddr_zos_s390x.s
│ │ ├── zsyscall_aix_ppc.go
│ │ ├── zsyscall_aix_ppc64.go
│ │ ├── zsyscall_aix_ppc64_gc.go
│ │ ├── zsyscall_aix_ppc64_gccgo.go
│ │ ├── zsyscall_darwin_amd64.go
│ │ ├── zsyscall_darwin_amd64.s
│ │ ├── zsyscall_darwin_arm64.go
│ │ ├── zsyscall_darwin_arm64.s
│ │ ├── zsyscall_dragonfly_amd64.go
│ │ ├── zsyscall_freebsd_386.go
│ │ ├── zsyscall_freebsd_amd64.go
│ │ ├── zsyscall_freebsd_arm.go
│ │ ├── zsyscall_freebsd_arm64.go
│ │ ├── zsyscall_freebsd_riscv64.go
│ │ ├── zsyscall_illumos_amd64.go
│ │ ├── zsyscall_linux.go
│ │ ├── zsyscall_linux_386.go
│ │ ├── zsyscall_linux_amd64.go
│ │ ├── zsyscall_linux_arm.go
│ │ ├── zsyscall_linux_arm64.go
│ │ ├── zsyscall_linux_loong64.go
│ │ ├── zsyscall_linux_mips.go
│ │ ├── zsyscall_linux_mips64.go
│ │ ├── zsyscall_linux_mips64le.go
│ │ ├── zsyscall_linux_mipsle.go
│ │ ├── zsyscall_linux_ppc.go
│ │ ├── zsyscall_linux_ppc64.go
│ │ ├── zsyscall_linux_ppc64le.go
│ │ ├── zsyscall_linux_riscv64.go
│ │ ├── zsyscall_linux_s390x.go
│ │ ├── zsyscall_linux_sparc64.go
│ │ ├── zsyscall_netbsd_386.go
│ │ ├── zsyscall_netbsd_amd64.go
│ │ ├── zsyscall_netbsd_arm.go
│ │ ├── zsyscall_netbsd_arm64.go
│ │ ├── zsyscall_openbsd_386.go
│ │ ├── zsyscall_openbsd_386.s
│ │ ├── zsyscall_openbsd_amd64.go
│ │ ├── zsyscall_openbsd_amd64.s
│ │ ├── zsyscall_openbsd_arm.go
│ │ ├── zsyscall_openbsd_arm.s
│ │ ├── zsyscall_openbsd_arm64.go
│ │ ├── zsyscall_openbsd_arm64.s
│ │ ├── zsyscall_openbsd_mips64.go
│ │ ├── zsyscall_openbsd_mips64.s
│ │ ├── zsyscall_openbsd_ppc64.go
│ │ ├── zsyscall_openbsd_ppc64.s
│ │ ├── zsyscall_openbsd_riscv64.go
│ │ ├── zsyscall_openbsd_riscv64.s
│ │ ├── zsyscall_solaris_amd64.go
│ │ ├── zsyscall_zos_s390x.go
│ │ ├── zsysctl_openbsd_386.go
│ │ ├── zsysctl_openbsd_amd64.go
│ │ ├── zsysctl_openbsd_arm.go
│ │ ├── zsysctl_openbsd_arm64.go
│ │ ├── zsysctl_openbsd_mips64.go
│ │ ├── zsysctl_openbsd_ppc64.go
│ │ ├── zsysctl_openbsd_riscv64.go
│ │ ├── zsysnum_darwin_amd64.go
│ │ ├── zsysnum_darwin_arm64.go
│ │ ├── zsysnum_dragonfly_amd64.go
│ │ ├── zsysnum_freebsd_386.go
│ │ ├── zsysnum_freebsd_amd64.go
│ │ ├── zsysnum_freebsd_arm.go
│ │ ├── zsysnum_freebsd_arm64.go
│ │ ├── zsysnum_freebsd_riscv64.go
│ │ ├── zsysnum_linux_386.go
│ │ ├── zsysnum_linux_amd64.go
│ │ ├── zsysnum_linux_arm.go
│ │ ├── zsysnum_linux_arm64.go
│ │ ├── zsysnum_linux_loong64.go
│ │ ├── zsysnum_linux_mips.go
│ │ ├── zsysnum_linux_mips64.go
│ │ ├── zsysnum_linux_mips64le.go
│ │ ├── zsysnum_linux_mipsle.go
│ │ ├── zsysnum_linux_ppc.go
│ │ ├── zsysnum_linux_ppc64.go
│ │ ├── zsysnum_linux_ppc64le.go
│ │ ├── zsysnum_linux_riscv64.go
│ │ ├── zsysnum_linux_s390x.go
│ │ ├── zsysnum_linux_sparc64.go
│ │ ├── zsysnum_netbsd_386.go
│ │ ├── zsysnum_netbsd_amd64.go
│ │ ├── zsysnum_netbsd_arm.go
│ │ ├── zsysnum_netbsd_arm64.go
│ │ ├── zsysnum_openbsd_386.go
│ │ ├── zsysnum_openbsd_amd64.go
│ │ ├── zsysnum_openbsd_arm.go
│ │ ├── zsysnum_openbsd_arm64.go
│ │ ├── zsysnum_openbsd_mips64.go
│ │ ├── zsysnum_openbsd_ppc64.go
│ │ ├── zsysnum_openbsd_riscv64.go
│ │ ├── zsysnum_zos_s390x.go
│ │ ├── ztypes_aix_ppc.go
│ │ ├── ztypes_aix_ppc64.go
│ │ ├── ztypes_darwin_amd64.go
│ │ ├── ztypes_darwin_arm64.go
│ │ ├── ztypes_dragonfly_amd64.go
│ │ ├── ztypes_freebsd_386.go
│ │ ├── ztypes_freebsd_amd64.go
│ │ ├── ztypes_freebsd_arm.go
│ │ ├── ztypes_freebsd_arm64.go
│ │ ├── ztypes_freebsd_riscv64.go
│ │ ├── ztypes_linux.go
│ │ ├── ztypes_linux_386.go
│ │ ├── ztypes_linux_amd64.go
│ │ ├── ztypes_linux_arm.go
│ │ ├── ztypes_linux_arm64.go
│ │ ├── ztypes_linux_loong64.go
│ │ ├── ztypes_linux_mips.go
│ │ ├── ztypes_linux_mips64.go
│ │ ├── ztypes_linux_mips64le.go
│ │ ├── ztypes_linux_mipsle.go
│ │ ├── ztypes_linux_ppc.go
│ │ ├── ztypes_linux_ppc64.go
│ │ ├── ztypes_linux_ppc64le.go
│ │ ├── ztypes_linux_riscv64.go
│ │ ├── ztypes_linux_s390x.go
│ │ ├── ztypes_linux_sparc64.go
│ │ ├── ztypes_netbsd_386.go
│ │ ├── ztypes_netbsd_amd64.go
│ │ ├── ztypes_netbsd_arm.go
│ │ ├── ztypes_netbsd_arm64.go
│ │ ├── ztypes_openbsd_386.go
│ │ ├── ztypes_openbsd_amd64.go
│ │ ├── ztypes_openbsd_arm.go
│ │ ├── ztypes_openbsd_arm64.go
│ │ ├── ztypes_openbsd_mips64.go
│ │ ├── ztypes_openbsd_ppc64.go
│ │ ├── ztypes_openbsd_riscv64.go
│ │ ├── ztypes_solaris_amd64.go
│ │ └── ztypes_zos_s390x.go
│ └── windows/
│ ├── aliases.go
│ ├── dll_windows.go
│ ├── env_windows.go
│ ├── eventlog.go
│ ├── exec_windows.go
│ ├── memory_windows.go
│ ├── mkerrors.bash
│ ├── mkknownfolderids.bash
│ ├── mksyscall.go
│ ├── race.go
│ ├── race0.go
│ ├── security_windows.go
│ ├── service.go
│ ├── setupapi_windows.go
│ ├── str.go
│ ├── syscall.go
│ ├── syscall_windows.go
│ ├── types_windows.go
│ ├── types_windows_386.go
│ ├── types_windows_amd64.go
│ ├── types_windows_arm.go
│ ├── types_windows_arm64.go
│ ├── zerrors_windows.go
│ ├── zknownfolderids_windows.go
│ └── zsyscall_windows.go
└── term/
├── CONTRIBUTING.md
├── LICENSE
├── PATENTS
├── README.md
├── codereview.cfg
├── term.go
├── term_plan9.go
├── term_unix.go
├── term_unix_bsd.go
├── term_unix_other.go
├── term_unsupported.go
├── term_windows.go
└── terminal.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .DEREK.yml
================================================
curators:
- alexellis
- welteki
- rgee0
features:
- dco_check
- comments
- pr_description_required
- release_notes
custom_messages:
- name: k3s
value: |
--
Thank you for your interest in k3sup.
This issue appears to be a problem with the upstream k3s project
which k3sup installs and automates.
Please raise an issue on [Rancher's k3s repository](https://github.com/rancher/k3s/) and
link it back to this issue for tracking purposes. If Rancher determines that this is
actually an error with k3sup, then please feel free to return and re-open the issue, or
to comment again.
- name: template
value: |
This project uses Issue and PR templates and requires that all
users fill out the template in detail before help can be given.
To continue please edit your Issue/PR or open a new one, and
please provide all the fields requested.
Thank you for your contribution.
- name: propose
value: |
This project follows a contributing guide which states that all
changes must be proposed with an Issue before being worked on.
Please raise an Issue and update your Pull Request to include
the ID or link as part of the description.
Thank you for your contribution.
- name: test
value: |
This project follows a contributing guide which requires that
all changes are tested before being merged. You should include
worked examples that a maintainer can run to prove that the
changes are good.
Screenshots and command line output are also accepted, but
must show the positive, and negative cases, not just that
what was added worked as you expected.
Thank you for your contribution.
contributing_url: https://github.com/alexellis/arkade/blob/master/CONTRIBUTING.md
================================================
FILE: .github/CODEOWNERS
================================================
@alexellis
================================================
FILE: .github/FUNDING.yml
================================================
custom: ["https://slicervm.com"]
================================================
FILE: .github/ISSUE_TEMPLATE/report-an-issue.md
================================================
---
name: Report an issue
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Expected behaviour**
What did you expect to happen?
**Current behaviour**
What happened?
**To Reproduce**
Include full unabridged instructions that anyone can use to reproduce the problem
**Screenshots / console output**
Add a screenshot or console output
**Versions:**
- OS:
- K3sup Version:
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/request-a-feature.md
================================================
---
name: Request a feature
about: Request a new feature or change in this project
title: ''
labels: ''
assignees: ''
---
**What do you want?**
Focus on the feature, not the technical approach or solution.
**Why do you want this?**
Make a case for why your request should be considered.
**Recommended solution**
If you are aware of one, share your preferred solution here
**Additional context**
Share any other context you feel is relevant or adds to your case.
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## Why do you need this?
## Expected Behaviour
## Current Behaviour
## Possible Solution
## Steps to Reproduce
1.
2.
3.
4.
## Your Environment
* k3sup version:
```
k3sup version
```
* What Kubernetes distribution, client and server version are you using?
```
kubectl version
```
* What OS or type or VM are you using for your cluster? Where is it hosted? (for `k3sup install/join`):
* Operating System and version (e.g. Linux, Windows, MacOS):
```
uname -a
cat /etc/os-release
```
## Do you want to work on this?
Subject to design approval, are you willing to work on a Pull Request for this issue or feature request?
- [ ] Yes
- [ ] No
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Why do you need this?
- [ ] I have raised an issue to propose this change ([required](https://github.com/openfaas/faas/blob/master/CONTRIBUTING.md))
If you have no approval from a maintainer, close this PR and raise an issue.
## Description
## How Has This Been Tested?
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist:
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I've read the [CONTRIBUTION](https://github.com/alexellis/arkade/blob/master/CONTRIBUTING.md) guide
- [ ] I have signed-off my commits with `git commit -s`
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
================================================
FILE: .github/workflows/build.yaml
================================================
name: build
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
jobs:
build:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: "1.26.x"
- name: Make all
run: make all
================================================
FILE: .github/workflows/publish.yaml
================================================
name: publish
on:
push:
tags:
- '*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: "1.26.x"
- name: Make all
run: make all
- name: Upload release binaries
uses: alexellis/upload-assets@0.4.1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./bin/k3sup*"]'
================================================
FILE: .gitignore
================================================
k3sup
bin/**
kubeconfig
.DS_Store
.idea/
mc
/install.sh
/*.json
*.txt
/bootstrap.sh
================================================
FILE: EULA.md
================================================
End User License Agreement (EULA) for K3sup Pro
Note: K3sup CE is licensed under the MIT license, see LICENSE for details.
1. Licensed Software
1.1 K3sup Pro (the “Licensed Software”) is licensed as commercial software and must not be used without a valid license key issued by OpenFaaS Ltd.
1.2 K3sup Pro may be offered in different editions or variants from time to time. The term “K3sup Pro” in this Agreement refers to any commercially licensed edition or variant of K3sup Pro provided by OpenFaaS Ltd.
1.3 OpenFaaS Ltd (“Supplier”) is a company registered in England & Wales, company number: 11076587, registered address: Peterborough, UK.
2. Your Agreement
2.1 By accessing, executing, or otherwise using the Licensed Software, you (“Customer”) acknowledge that you have read this Agreement, understand it, and agree to be bound by its terms and conditions. If you are not willing to be bound by the terms of this Agreement, do not access or use the Licensed Software.
2.2 If you are using the Licensed Software in your capacity as employee or agent of a company or organization, then any references to “you” in this Agreement shall refer to such entity and not to you in your personal capacity. You warrant that you are authorized to legally bind the company or organization on whose behalf you are accessing the Licensed Software. If you are not so authorized, then neither you nor your company or organization may use the Licensed Software in any manner whatsoever.
2.3 This Agreement, including any supplemental terms, is between you (“Customer”) and OpenFaaS Ltd (“Supplier”).
2.4 Governing law. This Agreement is entered into under the jurisdiction of the Courts of England and Wales and shall be governed by, and construed in accordance with, the laws of England and Wales, exclusive of its choice of law rules.
3. No Free Trial
3.1 No free trial is offered or available for K3sup Pro. Access to and use of the Licensed Software requires a valid, paid license at all times.
3.2 Any evaluation use of K3sup Pro may only occur under a separate, written evaluation agreement executed by Supplier in its sole discretion. In the absence of such a written evaluation agreement, you must not use the Licensed Software for any evaluation or trial purposes.
4. Grant of License; Ownership; Restrictions; Feedback
4.1 License Grant. Subject to the terms and conditions of this Agreement and any applicable order form, invoice, quote, or checkout confirmation (each an “Order Form”), Supplier grants to Customer a limited, non-exclusive, non-transferable, revocable license to install and use the Licensed Software solely for Customer’s internal purposes and only for the term and in accordance with the conditions and limitations set forth herein and in the applicable Order Form.
4.2 Third-Party and Open Source Software. To the extent that there is any third-party software embedded in, bundled with, or otherwise provided to Customer in connection with the Licensed Software (“Third Party Software”), such Third Party Software shall be used solely with the operation of the Licensed Software and not as a standalone application or for any other purpose. Certain Third Party Software may be subject to an open source license (“OSS License”). Customer’s rights with respect to such components are governed by the applicable OSS License; nothing in this Agreement shall restrict, limit, or otherwise affect any rights or obligations Customer may have under such OSS License. This includes, without limitation, the Apache License, Version 2.0 and the MIT License.
4.3 Ownership. As between the parties, Supplier retains all right, title, and interest in and to the Licensed Software and all related materials, including all intellectual property rights therein, whether now existing or later arising.
4.4 Intellectual Property Rights. Copyright for samples, code, logos, trademarks, diagrams, and documentation rests with Supplier. All pre-existing intellectual property remains the property of the originating party; no intellectual property is transferred from Customer to Supplier under this Agreement.
4.5 Restrictions. Except to the extent expressly permitted by applicable law and only to the extent Supplier is not permitted by that applicable law to exclude or limit such rights, Customer shall not (and shall not permit any third party to) distribute, display, sublicense, rent, lease, lend, timeshare, use in a service bureau, modify, translate, reverse engineer, decompile, disassemble, create derivative works based on, or copy the Licensed Software or related documentation. Customer shall not remove, alter, or obscure any proprietary notices or labels on the Licensed Software.
4.6 Feedback. Customer may provide feedback to Supplier about the Licensed Software (including suggestions or enhancement requests). Supplier may develop, modify, and improve the Licensed Software based on Customer’s feedback without obligation to Customer, and Customer irrevocably assigns to Supplier all right, title, and interest in such feedback.
5. Licensing and Use Rights
5.1 Personal Use via GitHub Sponsorship. Personal use is permitted through an active GitHub Sponsorship to either @alexellis or @openfaas on GitHub at a minimum of 25 USD per month. Termination, lapse, or downgrade of Sponsorship below this minimum immediately terminates the license. Personal use counts only for installation to privately owned hardware, or rented cloud hosts, which are in no way connected to or operated for or on the behalf of a business. Personal licenses are for a single named individual, non-transferable, and may not be used to provide services to, or on behalf of, any business, organization, or client.
5.2 Commercial Use. Commercial use requires a paid license per user (seat). A minimum of five (5) seats is required and licenses are paid annually via ACH in USD or SWIFT in GBP. To request a commercial license, email [contact@openfaas.com](mailto:contact@openfaas.com). Contractors, consultants, managed service providers, and any use on infrastructure connected to or operated for or on behalf of a business require a commercial license.
5.3 Priority of Terms. If there is any conflict between this Section 5 and any Order Form, the Order Form shall prevail solely with respect to the quantities, term, and pricing stated therein.
6. Termination and Continuing Obligations; Renewal Responsibility
6.1 Term. This Agreement is effective from the first date you install, access, or use the Licensed Software and continues until terminated as set forth below.
6.2 By Customer. Customer may terminate this Agreement at any time by permanently deleting the Licensed Software, destroying all copies, and ceasing all use.
6.3 By Supplier. Supplier may terminate this Agreement immediately upon written notice if Customer fails to comply with any terms or conditions herein, including use without a valid license (including a lapsed Sponsorship for personal use) or use beyond the scope or term of the license granted.
6.4 Automatic Termination. This Agreement terminates automatically without notice upon expiry of the license term, failure to renew, or termination of the qualifying GitHub Sponsorship for personal use.
6.5 Effect of Termination. Upon termination, Customer must immediately stop using the Licensed Software, delete all copies in its possession or control, and confirm in writing to Supplier that these actions have been completed. Sections 1, 2, 4, 5, 7, 8, and 9 survive termination.
6.6 Renewal Responsibility. It is the sole responsibility of Customer to renew and remit payment for any licenses in a timely manner to maintain uninterrupted access and compliance. Supplier recommends requesting a renewal quote at least thirty (30) days prior to the expiry of the current term. Supplier bears no costs, liabilities, or obligations related to Customer’s failure to renew or remit payment; any interruption due to non-renewal or non-payment does not entitle Customer to any remedies or compensation.
7. Customer Data; Audit
7.1 Account Data. To use the Licensed Software, Customer may need to provide contact and billing information (“Account Data”). Customer must provide complete and accurate Account Data and keep it up to date. By providing Account Data, Customer consents to receive communications from Supplier regarding the Licensed Software and other Supplier products. Customer may opt out of marketing communications by contacting [contact@openfaas.com](mailto:contact@openfaas.com).
7.2 Audit. Supplier may audit Customer’s use of the Licensed Software to assess compliance with this Agreement. Customer agrees to cooperate and provide reasonable assistance and access to relevant records (including, for example, purchase records, deployment records, and license key usage logs). Any audit shall not unreasonably interfere with Customer’s normal business operations.
8. Co-Marketing
8.1 At the request of Supplier, Customer agrees to participate in reasonable marketing activities that promote the benefits of the Licensed Software to other potential customers, which may include providing testimonials, case studies, and references.
8.2 Customer grants Supplier the right to use Customer’s name and logo on Supplier’s websites and in Supplier’s promotional materials.
8.3 Customer agrees that Supplier may disclose Customer as a customer of the Licensed Software.
9. Limitation of Liability; Disclaimer
9.1 Warranty Disclaimer. The Licensed Software and documentation are provided “as is” and “as available” without warranty of any kind, express or implied. Customer uses the Licensed Software at its own risk. Customer assumes all responsibility for selecting the Licensed Software to achieve its intended results and for the installation of, and results obtained from, the Licensed Software.
9.2 No Consequential Damages. IN NO EVENT SHALL SUPPLIER BE LIABLE HEREUNDER FOR SPECIAL, PUNITIVE, INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES, INCLUDING, BUT NOT LIMITED TO, LOSS OF PROFITS, LOSS OF REVENUE, LOSS OF USE, OR LOSS OF DATA, EVEN IF ADVISED OF THE POSSIBILITY THEREOF OR, IF REASONABLY FORESEEABLE, INCURRED BY CUSTOMER OR END USERS, OR CLAIMED AGAINST CUSTOMER BY ANY OTHER PARTY (WHETHER ANY SUCH CLAIMS ARISE UNDER THEORY OF CONTRACT, TORT, OR OTHERWISE).
9.3 Liability Cap. To the extent permitted by applicable law, the aggregate liability of Supplier and its licensors, personnel, subcontractors, and suppliers arising out of or related to this Agreement shall not exceed the license fees paid by Customer hereunder in the twelve (12) months immediately preceding the event giving rise to the claim.
10. Contact
If you have any questions about these terms or the Licensed Software, contact Supplier at [contact@openfaas.com](mailto:contact@openfaas.com).
================================================
FILE: LICENSE
================================================
There are two editions of K3sup.
K3sup Pro
Copyright (c) 2025 Alex Ellis, OpenFaaS Ltd
License: Proprietary - K3sup Pro EULA.
K3sup CE
MIT License
Copyright (c) 2025 Alex Ellis
Exclusions: assets, images and logos.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
Version := $(shell git describe --tags --dirty)
# Version := "dev"
GitCommit := $(shell git rev-parse HEAD)
LDFLAGS := "-s -w -X github.com/alexellis/k3sup/cmd.Version=$(Version) -X github.com/alexellis/k3sup/cmd.GitCommit=$(GitCommit)"
export GO111MODULE=on
SOURCE_DIRS = main.go cmd pkg
.PHONY: all
all: gofmt test dist hash
.PHONY: test
test:
CGO_ENABLED=0 go test $(shell go list ./... | grep -v /vendor/|xargs echo) -cover
.PHONY: gofmt
gofmt:
@test -z $(shell gofmt -l $(SOURCE_DIRS) ./ | grep -v vendor/| tee /dev/stderr) || (echo "[WARN] Fix formatting issues with 'make gofmt'" && exit 1)
.PHONY: dist
dist:
mkdir -p bin/
rm -rf bin/k3sup*
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags $(LDFLAGS) -o bin/k3sup
GOARM=7 GOARCH=arm CGO_ENABLED=0 GOOS=linux go build -ldflags $(LDFLAGS) -o bin/k3sup-armhf
GOARCH=arm64 CGO_ENABLED=0 GOOS=linux go build -ldflags $(LDFLAGS) -o bin/k3sup-arm64
CGO_ENABLED=0 GOOS=darwin go build -ldflags $(LDFLAGS) -o bin/k3sup-darwin
GOARCH=arm64 CGO_ENABLED=0 GOOS=darwin go build -ldflags $(LDFLAGS) -o bin/k3sup-darwin-arm64
GOOS=windows CGO_ENABLED=0 go build -ldflags $(LDFLAGS) -o bin/k3sup.exe
.PHONY: hash
hash:
rm -rf bin/*.sha256 && ./hack/hashgen.sh
================================================
FILE: README.md
================================================
# k3sup 🚀 (said 'ketchup')
k3sup is a light-weight utility to get from zero to KUBECONFIG with [k3s](https://k3s.io/) on any local or remote VM. All you need is `ssh` access and the `k3sup` binary to get `kubectl` access immediately.
The tool is written in Go and is cross-compiled for Linux, Windows, MacOS and even on Raspberry Pi.
How do you say it? Ketchup, as in tomato.
**Introducing K3sup Pro 🎉**
Whilst the CE edition is ideal for experimentation, [`k3sup-pro`](#k3sup-pro) was built to satisfy long standing requests for an IaaC/GitOps experience.
`k3sup-pro` adds a `plan` and `apply` command to automate installations both small and large - running in parallel. The plan file can be customised and retained in Git for maintenance and updates.
[](https://opensource.org/licenses/MIT)
[](https://github.com/alexellis/k3sup/actions/workflows/build.yaml)
[]()
## Contents:
- [k3sup 🚀 (said 'ketchup')](#k3sup--said-ketchup)
- [Contents:](#contents)
- [What's this for? 💻](#whats-this-for-)
- [Use-cases](#use-cases)
- [Bootstrapping Kubernetes](#bootstrapping-kubernetes)
- [Download `k3sup` CE (tl;dr)](#download-k3sup-ce-tldr)
- [Demo of K3sup CE📼](#demo-of-k3sup-ce)
- [Usage ✅](#usage-)
- [Pre-requisites for k3sup servers and agents](#pre-requisites-for-k3sup-servers-and-agents)
- [K3sup Pro](#k3sup-pro)
- [Getting `k3sup-pro`](#getting-k3sup-pro)
- [Activating K3sup Pro](#activating-k3sup-pro)
- [K3sup `plan` / `apply` for automation and large installations](#k3sup-plan--apply-for-automation-and-large-installations)
- [Rapid uninstallation / reset with `k3sup-pro uninstall`](#rapid-uninstallation--reset-with-k3sup-pro-uninstall)
- [K3sup `pro exec` - run a command everywhere](#k3sup-pro-exec---run-a-command-everywhere)
- [K3sup `pro get-config` - work with an existing cluster](#k3sup-pro-get-config---work-with-an-existing-cluster)
- [K3sup Community Edition (CE)](#k3sup-community-edition-ce)
- [👑 Setup a Kubernetes *server* with `k3sup`](#-setup-a-kubernetes-server-with-k3sup)
- [Checking if a cluster is ready](#checking-if-a-cluster-is-ready)
- [Merging clusters into your KUBECONFIG](#merging-clusters-into-your-kubeconfig)
- [😸 Join some agents to your Kubernetes server](#-join-some-agents-to-your-kubernetes-server)
- [Use your hardware authentication / 2FA or SSH Agent](#use-your-hardware-authentication--2fa-or-ssh-agent)
- [Create a multi-master (HA) setup with external SQL](#create-a-multi-master-ha-setup-with-external-sql)
- [Create a multi-master (HA) setup with embedded etcd](#create-a-multi-master-ha-setup-with-embedded-etcd)
- [👨💻 Micro-tutorial for Raspberry Pi (2, 3, or 4) 🥧](#-micro-tutorial-for-raspberry-pi-2-3-or-4-)
- [Caveats on security](#caveats-on-security)
- [Contributing](#contributing)
- [Blog posts & tweets](#blog-posts--tweets)
- [Contributing via GitHub](#contributing-via-github)
- [License](#license)
- [📢 What are people saying about `k3sup`?](#-what-are-people-saying-about-k3sup)
- [Similar tools & glossary](#similar-tools--glossary)
- [Troubleshooting and support](#troubleshooting-and-support)
- [Maybe the problem is with K3s?](#maybe-the-problem-is-with-k3s)
- [Common issues](#common-issues)
- [Getting access to your KUBECONFIG](#getting-access-to-your-kubeconfig)
- [Smart cards and 2FA](#smart-cards-and-2fa)
- [Misc note on `iptables`](#misc-note-on-iptables)
## What's this for? 💻
This tool uses `ssh` to install `k3s` to a remote Linux host. You can also use it to join existing Linux hosts into a k3s cluster as `agents`. First, `k3s` is installed using the utility script from Rancher, along with a flag for your host's public IP so that TLS works properly. The `kubeconfig` file on the server is then fetched and updated so that you can connect from your laptop using `kubectl`.
You may wonder why a tool like this needs to exist when you can do this sort of thing with bash.
k3sup was developed to automate what can be a very manual and confusing process for many developers, who are already short on time. Once you've provisioned a VM with your favourite tooling, `k3sup` means you are only 60 seconds away from running `kubectl get pods` on your own computer. If you are a local computer, you can bypass SSH with `k3sup install --local`
**How does k3sup work?**
```
k3sup install
+---------------------+ +-----------------------------+
| | 1. SSH | |
| Your Laptop / +---------------->| Remote Server (VM/RPi) |
| Workstation | | |
| | 2. Install | +---------------------+ |
| +----------------+ | k3s -------->| | k3s (server/agent) | |
| | kubectl | | | +---------------------+ |
| +----------------+ | 3. Fetch | |
| | kubeconfig |<-+ kubeconfig | |
| +----------------+ | +-----------------------------+
| |
+---------------------+ k3sup join
+-------------------->+-----------+
| | Agent 1 |
+-------------------->+-----------+
| | Agent 2 |
+-------------------->+-----------+
| Agent n |
+-----------+
```
* Step 1: k3sup install → SSH into server, install k3s
* Step 2: kubeconfig → Fetched to your laptop automatically
* Step 3: k3sup join → Add agents to the cluster via SSH
* Step 4: kubectl → Ready to use from your laptop 🚀
> **Tip:** Create clusters on Mac, Linux + WSL2 with K3sup and [SlicerVM](https://slicervm.com)
### Use-cases
K3sup runs from your local machine, without ever having to log into a remote server.
* Bootstrap Kubernetes with k3s onto any VM with `k3sup install` - either manually, during CI or through `cloud-init`
* Get from zero to `kubectl` with `k3s` on bare-metal, Raspberry Pi (RPi), VMs, AWS EC2, Google Cloud, DigitalOcean, Civo, Linode, Scaleway, and others
* Build a Highly-Available (HA), multi-master (server) cluster
* Fetch the KUBECONFIG from an existing cluster with `k3sup-pro get-config`
* Join nodes into an existing `k3s` cluster with `k3sup join`
* Build a massive cluster for automation and scale-out testing using `k3sup plan` and a JSON file with IP addresses
### Bootstrapping Kubernetes

*Conceptual architecture, showing `k3sup` running locally against any VM such as AWS EC2 or a VPS such as DigitalOcean.*
### Download `k3sup` CE (tl;dr)
`k3sup` is distributed as a static Go binary. You can use the installer on MacOS and Linux, or visit the [Releases page](https://github.com/alexellis/k3sup/releases) to download the executable for Windows.
```sh
curl -sLS https://get.k3sup.dev | sh
sudo install k3sup /usr/local/bin/
k3sup --help
```
> A note for Windows users. Windows users can use `k3sup install` and `k3sup join` with a normal "Windows command prompt".
## Demo of K3sup CE📼
In the demo I install Kubernetes (`k3s`) onto two separate machines and get my `kubeconfig` downloaded to my laptop each time in around one minute.
1) Ubuntu 18.04 VM created on DigitalOcean with ssh key copied automatically
2) Raspberry Pi 4 with my ssh key copied over via `ssh-copy-id`
Watch the demo:
[](https://asciinema.org/a/262630)
## Usage ✅
The `k3sup` tool is a client application which you can run on your own computer. It uses SSH to connect to remote servers and creates a local KUBECONFIG file on your disk. Binaries are provided for MacOS, Windows, and Linux (including ARM).
### Pre-requisites for k3sup servers and agents
Some Linux hosts are configured to allow `sudo` to run without having to repeat your password. For those which are not already configured that way, you'll need to make the following changes if you wish to use `k3sup`:
```bash
# sudo visudo
# Then add to the bottom of the file
# replace "alex" with your username i.e. "ubuntu"
alex ALL=(ALL) NOPASSWD: ALL
```
In most circumstances, cloud images for Ubuntu and other distributions will not require this step.
As an alternative, if you only need a single server you can log in interactively and run `k3sup install --local` instead of using SSH.
## K3sup Pro
K3sup Pro is available for individuals via a [GitHub Sponsorship of 25+ USD / mo](https://github.com/sponsors/alexellis) and separately for commercial use. Review the [EULA](/EULA.md) before downloading or using the software.
> K3sup Pro is free with your [SlicerVM](https://slicervm.com) subscription. Slicer makes it quick and easy to spin up single or multi-node K3s clusters directly on your own computer, cloud, or homelab environments.
Support for all K3sup Pro users is provided by the Issue Tracker for the [K3sup CE repository](https://github.com/alexellis/k3sup-pro/).
* `activate` - obtain/refresh a license key. Commercial users just place your key at `~/.k3sup/LICENSE`
* `plan` - take one or more JSON files and generate a YAML plan for a HA installation of K3s
* `apply` - run the installation in parallel, optionally pre-downloading the K3s binary and copying it via SSH beforehand
* `exec` - run a command on all nodes in the cluster
* `get-config` - get a kubeconfig from an existing installation
* `uninstall` - uninstall k3s from all nodes in the cluster in parallel
Classic K3sup CE commands are also available within the single binary, for backwards compatibility and for quick testing.
* `install` - install K3s to a single node imperatively
* `join` - join a single node to an existing K3s server
The `--predownload` flag for `k3sup-pro apply` is the first step towards a fully airgapped solution, and reduces bandwidth whilst speeding up installation.
Walkthrough of `plan`, `apply`, `get-config` and `exec`:
[](https://asciinema.org/a/725554)
### K3sup Pro roadmap
The initial version of K3sup Pro is largely feature-complete, however there are some additional features planned for commercial users:
* Use K3sup Pro Plan/Apply via bastion hosts
* Airgapped installation via initial download of packages on a local machine
### Getting `k3sup-pro`
The `k3sup-pro` binary is packaged in a container image, rather than being downloaded via GitHub Releases.
The recommended option is to use K3sup CE to obtain K3sup Pro:
```bash
PRO=1 curl -sLS https://get.k3sup.dev | sudo -E sh
```
Or, if you already have the latest K3sup CE version, it can replace itself:
```bash
sudo k3sup get pro
```
For the `k3sup get pro` command, you can omit `sudo` by passing a `--path` variable to a writeable location by your user.
You can browse specific versions at [ghcr.io/openfaasltd/k3sup-pro](https://ghcr.io/openfaasltd/k3sup-pro) then pass the `--version` flag accordingly.
### Activating K3sup Pro
Run `k3sup-pro activate` to verify your identity using GitHub.com. You'll only need to do this on your laptop/workstation - machines which will host K3s do not need any additional steps.
Commercial users can place their license key at `$HOME/.k3sup/LICENSE` and do not need to run `k3sup-pro activate`.
### K3sup `plan` / `apply` for automation and large installations
The `k3sup-pro plan` command reads a set of JSON files containing your hosts, and will generate a YAML plan file that you can edit to customize the installation.
Example input file:
```json
[
{
"hostname": "k3s-server-1",
"ip": "192.168.129.138"
},
{
"hostname": "k3s-server-2",
"ip": "192.168.129.128"
},
{
"hostname": "k3s-server-3",
"ip": "192.168.129.131"
},
{
"hostname": "k3s-agent-1",
"ip": "192.168.129.130"
},
{
"hostname": "k3s-agent-2",
"ip": "192.168.129.127"
}
]
```
The following will create 1x primary server, with 2x additional servers within a HA etcd cluster, the last two nodes will be added as agents.
```bash
k3sup pro plan ./n100.json ./n200.json \
--user ubuntu \
--servers 3 \
--svclb=false \
--server-extra-args "--disable traefik" \
--agent-extra-args "--node-label worker=true"
```
Example plan.yaml file:
```yaml
version:
k3s_channel: stable
server_options:
user: ubuntu
ssh_port: 22
k3s_extra_args: --disable traefik
parallel: 5
traefik: true
agent_options:
k3s_extra_args: --node-label worker=true
hosts:
- name: k3s-1
role: server
host: 192.168.138.2
architecture: x86_64
- name: k3s-2
role: server
host: 192.168.138.3
architecture: x86_64
- name: k3s-3
role: server
host: 192.168.138.4
architecture: x86_64
- name: k3s-agent-1
role: agent
host: 192.168.137.2
architecture: x86_64
- name: k3s-agent-2
role: agent
host: 192.168.137.3
architecture: x86_64
- name: k3s-agent-3
role: agent
host: 192.168.137.4
architecture: x86_64
```
The YAML plan file can be edited and committed to Git for maintenance and future upgrades.
Then when you're ready to install, you can run `k3sup-pro apply` to install in parallel.
The `--predownload` flag will download the k3s binary to your local machine, then copy it over SSH to each host to speed up the installation.
The `--parallel` flag sets how many installation steps to run at the same time.
```bash
k3sup pro apply \
--predownload \
--parallel 10
```
You can also get hold of your kubeconfig with `k3sup-pro get-config` and then use `kubectl` to access your cluster.
Merge it into your main KUBECONFIG file:
```bash
k3sup pro get-config \
--local-path $HOME/.kube/config \
--context my-k3s \
--merge
```
Or set up a local file:
```bash
k3sup pro get-config \
--local-path ./kubeconfig
export KUBECONFIG=`pwd`/kubeconfig
```
Watch a demo with dozens of Firecracker VMs: [Testing Kubernetes at Scale with bare-metal](https://youtu.be/o4UxRw-Cc8c)
### Rapid uninstallation / reset with `k3sup-pro uninstall`
The `k3sup-pro uninstall` command will uninstall k3s from all nodes in the cluster.
If you have a plan YAML file, the username, SSH ports, and key files will all be read from the file, and the uninstallation will be performed in order and in parallel. Removing the agents first, then any additional servers, and finally the primary server.
```bash
k3sup pro uninstall
```
If you only have devices JSON files, you may also need a `--user` and / or `--ssh-key` flag.
```bash
k3sup pro uninstall \
--user ubuntu \
--ssh-key ~/.ssh/id_rsa
```
### K3sup `pro exec` - run a command everywhere
The `k3sup-pro exec` command allows you to run a command on all nodes in the cluster. You can specify `--servers` or `--agents` to run the command on only the servers or agents.
Run on all nodes:
```bash
k3sup pro exec \
"free -h"
```
Only on servers:
```bash
k3sup pro exec \
--servers \
"sudo journalctl -u k3s -n 100"
```
Only on agents:
```bash
k3sup pro exec \
--agents \
"sudo journalctl -u k3s-agent -n 100"
```
### K3sup `pro get-config` - work with an existing cluster
The `k3sup-pro get-config` command allows you to retrieve kubeconfig files from existing K3s installations without performing any installation steps. This is useful when you already have K3s running and just need to access the cluster configuration.
You can also use it if you initially created a local `./kubeconfig` file but now want to merge it under a meaningful context name to your main `$HOME/.kube/config` file.
Get kubeconfig from a remote server:
```bash
k3sup pro get-config \
--host 192.168.0.100 \
--user ubuntu \
--local-path ./kubeconfig
```
Get kubeconfig from a local installation:
```bash
k3sup pro get-config --local
```
Merge kubeconfig into your main KUBECONFIG file:
```bash
k3sup pro get-config \
--host 192.168.0.100 \
--user ubuntu \
--merge \
--local-path $HOME/.kube/config \
--context my-remote-cluster
```
Use a custom SSH key:
```bash
k3sup pro get-config \
--host 192.168.0.100 \
--user ubuntu \
--ssh-key $HOME/.ssh/my-key \
--local-path ./kubeconfig
```
If you do not have `k3sup-pro` yet, you can also use `k3sup install` with the `--skip-install` flag.
## K3sup Community Edition (CE)
The CE edition of K3sup is available to all users for free, and the code is licensed under the MIT license so you can also adapt it for your own use, or contribute back to the project.
The CE edition has been available since 2019 and has been used by many different people to learn about Kubernetes, and to build their own clusters using imperative bash commands.
### 👑 Setup a Kubernetes *server* with `k3sup`
You can setup a server and stop here, or go on to use the `join` command to add some "agents" aka `nodes` or `workers` into the cluster to expand its compute capacity.
Provision a new VM running a compatible operating system such as Ubuntu, Debian, Raspbian, or something else. Make sure that you opt-in to copy your registered SSH keys over to the new VM or host automatically.
> Note: You can copy ssh keys to a remote VM with `ssh-copy-id user@IP`.
Imagine the IP was `192.168.0.1` and the username was `ubuntu`, then you would run this:
* Run `k3sup`:
```sh
export IP=192.168.0.1
k3sup install --ip $IP --user ubuntu
# Or use a hostname and SSH key for EC2
export HOST="ec2-3-250-131-77.eu-west-1.compute.amazonaws.com"
k3sup install --host $HOST --user ubuntu \
--ssh-key $HOME/ec2-key.pem
```
Other options for `install`:
* `--cluster` - start this server in clustering mode using embedded etcd (embedded HA)
* `--skip-install` - if you already have k3s installed, you can just run this command to get the `kubeconfig`
* `--ssh-key` - specify a specific path for the SSH key for remote login
* `--local` - Perform a local install without using ssh
* `--local-path` - default is `./kubeconfig` - set the file where you want to save your cluster's `kubeconfig`. By default this file will be overwritten.
* `--merge` - Merge config into existing file instead of overwriting (e.g. to add config to the default kubectl config, use `--local-path ~/.kube/config --merge`).
* `--context` - default is `default` - set the name of the kubeconfig context.
* `--ssh-port` - default is `22`, but you can specify an alternative port i.e. `2222`
* `--no-extras` - disable "servicelb" and "traefik"
* `--k3s-extra-args` - Optional extra arguments to pass to k3s installer, wrapped in quotes, i.e. `--k3s-extra-args '--disable traefik'` or `--k3s-extra-args '--docker'`. For multiple args combine then within single quotes `--k3s-extra-args '--disable traefik --docker'`.
* `--k3s-version` - set the specific version of k3s, i.e. `v1.21.1`
* `--k3s-channel` - set a specific version of k3s based upon a channel i.e. `stable`
- `--ipsec` - Enforces the optional extra argument for k3s: `--flannel-backend` option: `ipsec`
* `--print-command` - Prints out the command, sent over SSH to the remote computer
* `--datastore` - used to pass a SQL connection-string to the `--datastore-endpoint` flag of k3s. You must use [the format required by k3s in the Rancher docs](https://rancher.com/docs/k3s/latest/en/installation/ha/).
See even more install options by running `k3sup install --help`.
* Now try the access:
```bash
export KUBECONFIG=`pwd`/kubeconfig
kubectl get node
```
Note that you should always use `pwd/` so that a full path is set, and you can change directory if you wish.
### Checking if a cluster is ready
There are various ways to confirm whether a cluster is ready to use.
K3sup runs the "kubectl get nodes" command using a KUBECONFIG file, and looks for the "Ready" status on each node, including agents/workers.
Install K3s directly on the node and then check if it's ready:
```bash
k3sup install \
--local \
--context localk3s
k3sup ready \
--context localk3s \
--kubeconfig ./kubeconfig
```
Check a remote server saved to a local file:
```bash
k3sup install \
--ip 192.168.0.101 \
--user pi
k3sup ready \
--context default \
--kubeconfig ./kubeconfig
```
Check a merged context in your default KUBECONFIG:
```bash
k3sup install \
--ip 192.168.0.101 \
--user pi \
--context pik3s \
--merge \
--local-path $HOME/.kube/config
# $HOME/.kube/config is a default for kubeconfig
k3sup ready --context pik3s
```
### Merging clusters into your KUBECONFIG
You can also merge the remote config into your main KUBECONFIG file `$HOME/.kube/config`, then use `kubectl config get-contexts` or `kubectx` to manage it.
The default "context" name for the remote k3s cluster is `default`, however you can override this as below.
For example:
```bash
k3sup install \
--ip $IP \
--user $USER \
--merge \
--local-path $HOME/.kube/config \
--context my-k3s
```
Here we set a context of `my-k3s` and also merge into our main local `KUBECONFIG` file, so we could run `kubectl config use-context my-k3s` or `kubectx my-k3s`.
### 😸 Join some agents to your Kubernetes server
Let's say that you have a server, and have already run the following:
```sh
export SERVER_IP=192.168.0.100
export USER=root
k3sup install --ip $SERVER_IP --user $USER
```
Next join one or more `agents` to the cluster:
```sh
export AGENT_IP=192.168.0.101
export SERVER_IP=192.168.0.100
export USER=root
k3sup join --ip $AGENT_IP --server-ip $SERVER_IP --user $USER
```
Please note that if you are using different usernames for SSH'ing to the agent and the server that you must provide the username for the server via the `--server-user` parameter.
That's all, so with the above command you can have a two-node cluster up and running, whether that's using VMs on-premises, using Raspberry Pis, 64-bit ARM or even cloud VMs on EC2.
### Use your hardware authentication / 2FA or SSH Agent
You may wish to use the `ssh-agent` utility if:
* Your SSH key is protected by a password, and you don't want to type it in for each k3sup command
* You use a hardware authentication device key like a [Yubico YubiKey](https://amzn.to/3ApXR82) to authenticate SSH sessions
Run the following to set `SSH_AUTH_SOCK`:
```
$ eval $(ssh-agent)
Agent pid 2641757
```
Optionally, if your key is encrypted, run: `ssh-add ~/.ssh/id_rsa`
Now run any `k3sup` command, and your SSH key will be requested from the ssh-agent instead of from the usual location.
You can also specify an SSH key with `--ssh-key` if you want to use a specific key-pair.
### Create a multi-master (HA) setup with external SQL
The easiest way to test out k3s' multi-master (HA) mode with external storage, is to set up a Mysql server using DigitalOcean's managed service.
* Get the connection string from your DigitalOcean dashboard, and adapt it
Before:
```
mysql://doadmin:80624d3936dfc8d2e80593@db-mysql-lon1-90578-do-user-6456202-0.a.db.ondigitalocean.com:25060/defaultdb?ssl-mode=REQUIRED
```
After:
```
mysql://doadmin:80624d3936dfc8d2e80593@tcp(db-mysql-lon1-90578-do-user-6456202-0.a.db.ondigitalocean.com:25060)/defaultdb
```
Note that we've removed `?ssl-mode=REQUIRED` and wrapped the host/port in `tcp()`.
```bash
export DATASTORE="mysql://doadmin:80624d3936dfc8d2e80593@tcp(db-mysql-lon1-90578-do-user-6456202-0.a.db.ondigitalocean.com:25060)/defaultdb
```
You can prefix this command with ` ` two spaces, to prevent it being cached in your bash history.
Generate a token used to encrypt data (If you already have a running node this can be retrieved by logging into a running node and looking in `/var/lib/rancher/k3s/server/token`)
```bash
# Best option for a token:
export TOKEN=$(openssl rand -base64 64)
# Fallback for no openssl, on a Linux host:
export TOKEN=$(tr -dc A-Za-z0-9 14s v1.19.6+k3s1
```
There are two ways to prevent a dependency on the IP address of any one host. The first is to create a TCP load-balancer in the cloud of your choice, the second is for you to create a DNS round-robbin record, which contains all of the IPs of your servers.
In your DigitalOcean dashboard, go to the Networking menu and click "Load Balancer", create one in the same region as your Droplets and SQL server. Select your two Droplets, i.e. `104.248.34.61` and `142.93.175.203`, and use `TCP` with port `6443`.
If you want to run `k3sup join` against the IP of the LB, then you should also add `TCP` port `22`
Make sure that the health-check setting is also set to `TCP` and port `6443`. Wait to get your IP, mine was: `174.138.101.83`
Save the LB into an environment variable:
```bash
export LB=174.138.101.83
```
Now use `ssh` to log into both of your servers, and edit their config files at `/etc/systemd/system/k3s.service`, update the lines `--tls-san` and the following address, to that of your LB:
```
ExecStart=/usr/local/bin/k3s \
server \
'--tls-san' \
'104.248.135.109' \
```
Becomes:
```
ExecStart=/usr/local/bin/k3s \
server \
'--tls-san' \
'174.138.101.83' \
```
Now run:
```bash
sudo systemctl daemon-reload && \
sudo systemctl restart k3s-agent
```
And repeat these steps on the other server.
You can update the agent manually, via ssh and edit `/etc/systemd/system/k3s-agent.service.env` on the host, or use `k3sup join` again, but only if you added port `22` to your LB:
```bash
k3sup join --user root --server-ip $LB --ip $AGENT1
```
Finally, regenerate your KUBECONFIG file with the LB's IP, instead of one of the servers:
```bash
k3sup install --skip-install --ip $LB
```
Log into the first server, and stop k3s `sudo systemctl stop k3s`, then check that kubectl still functions as expected:
```bash
export KUBECONFIG=`pwd`/kubeconfig
kubectl get node -o wide
NAME STATUS ROLES AGE VERSION
k3sup-1 NotReady master 23m v1.19.6+k3s1
k3sup-2 Ready master 25m v1.19.6+k3s1
k3sup-3 Ready 22m v1.19.6+k3s1
```
You've just simulated a failure of one of your masters/servers, and you can still access kubectl. Congratulations on building a resilient k3s cluster.
### Create a multi-master (HA) setup with embedded etcd
In k3s `v1.19.5+k3s1` a HA multi-master (multi-server in k3s terminology) configuration is available called "embedded etcd". A quorum of servers will be required, which means having an odd number of nodes and least three. [See more](https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/)
* Initialize the cluster with the first server
Note the `--cluster` flag
```sh
export SERVER_IP=192.168.0.100
export USER=root
k3sup install \
--ip $SERVER_IP \
--user $USER \
--cluster \
--k3s-version v1.19.1+k3s1
```
* Join each additional server
> Note the new `--server` flag
```sh
export USER=root
export SERVER_IP=192.168.0.100
export NEXT_SERVER_IP=192.168.0.101
k3sup join \
--ip $NEXT_SERVER_IP \
--user $USER \
--server-user $USER \
--server-ip $SERVER_IP \
--server \
--k3s-version v1.19.1+k3s1
```
Now check `kubectl get node`:
```sh
kubectl get node
NAME STATUS ROLES AGE VERSION
paprika-gregory Ready master 8m27s v1.19.2-k3s
cave-sensor Ready master 27m v1.19.2-k3s
```
If you used `--no-extras` on the initial installation you will also need to provide it on each join:
```sh
export USER=root
export SERVER_IP=192.168.0.100
export NEXT_SERVER_IP=192.168.0.101
k3sup join \
--ip $NEXT_SERVER_IP \
--user $USER \
--server-user $USER \
--server-ip $SERVER_IP \
--server \
--no-extras \
--k3s-version v1.19.1+k3s1
```
### 👨💻 Micro-tutorial for Raspberry Pi (2, 3, or 4) 🥧
In a few moments you will have Kubernetes up and running on your Raspberry Pi 2, 3 or 4. Stand by for the fastest possible install. At the end you will have a KUBECONFIG file on your local computer that you can use to access your cluster remotely.

*Conceptual architecture, showing `k3sup` running locally against bare-metal ARM devices.*
* [Download etcher.io](https://www.balena.io/etcher/) for your OS
* Flash an SD card using [Raspbian Lite](https://www.raspberrypi.org/downloads/raspbian/)
* Enable SSH by creating an empty file named `ssh` in the boot partition
* Generate an ssh-key if you don't already have one with `ssh-keygen` (hit enter to all questions)
* Find the RPi IP with `ping -c raspberrypi.local`, then set `export SERVER_IP=""` with the IP
* Enable container features in the kernel, by editing `/boot/cmdline.txt` (or `/boot/firmware/cmdline.txt` on Ubuntu)
* Add the following to the end of the line: ` cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory`
* Copy over your ssh key with: `ssh-copy-id pi@raspberrypi.local`
* Run `k3sup install --ip $SERVER_IP --user pi`
* Point at the config file and get the status of the node:
```sh
export KUBECONFIG=`pwd`/kubeconfig
kubectl get node -o wide
```
You now have `kubectl` access from your laptop to your Raspberry Pi running k3s.
If you want to join some nodes, run `export IP=""` for each additional RPi, followed by:
* `k3sup join --ip $IP --server-ip $SERVER_IP --user pi`
> Remember all these commands are run from your computer, not the RPi.
Now where next? I would recommend my detailed tutorial where I spend time looking at how to flash the SD card, deploy k3s, deploy OpenFaaS (for some useful microservices), and then get incoming HTTP traffic.
Try it now: [Will it cluster? K3s on Raspbian](https://blog.alexellis.io/test-drive-k3s-on-raspberry-pi/)
## Caveats on security
If you are using public cloud, then make sure you see the notes from the Rancher team on setting up a Firewall or Security Group.
k3s docs: [k3s configuration / open ports](https://rancher.com/docs/k3s/latest/en/installation/installation-requirements/#networking)
## Contributing
### Blog posts & tweets
Blogs posts, tutorials, and Tweets about k3sup (`#k3sup`) are appreciated. Please send a PR to the README.md file to add yours.
### Contributing via GitHub
Before contributing code, please see the [CONTRIBUTING guide](https://github.com/alexellis/arkade/blob/master/CONTRIBUTING.md). Note that k3sup uses the same guide [arkade](https://arkade.dev)
Both Issues and PRs have their own templates. Please fill out the whole template.
All commits must be signed-off as part of the [Developer Certificate of Origin (DCO)](https://developercertificate.org)
### License
MIT
## 📢 What are people saying about `k3sup`?
* [Five years of Raspberry Pi clusters](https://www.raspberrypi.org/blog/five-years-of-raspberry-pi-clusters/) - raspberrypi.org
* [Multi-master HA Kubernetes in < 5 minutes](https://blog.alexellis.io/multi-master-ha-kubernetes-in-5-minutes/) by Alex Ellis
* [Kubernetes Homelab with Raspberry Pi and k3sup](https://blog.alexellis.io/raspberry-pi-homelab-with-k3sup/)
* [Building a Kubernetes cluster on Raspberry Pi running Ubuntu server](https://medium.com/icetek/building-a-kubernetes-cluster-on-raspberry-pi-running-ubuntu-server-8fc4edb30963) by Jakub Czapliński
* [Multi-node Kubernetes on Civo in 5 minutes flat with k3sup!](https://www.civo.com/learn/kubernetes-on-civo-in-5-minutes-flat) - Civo Learn guide
* [Zero to k3s Kubeconfig in seconds on AWS EC2 with k3sup](https://rancher.com/blog/2019/k3s-kubeconfig-in-seconds) by Saiyam Pathak
* [Create a 3-node k3s cluster with k3sup & DigitalOcean](https://blog.alexellis.io/create-a-3-node-k3s-cluster-with-k3sup-digitalocean/)
* [Cheap k3s cluster using Amazon Lightsail](https://eamonbauman.com/2020/05/09/cheap-k3s-cluster-using-amazon-lightsail/)
* [k3sup mentioned on Kubernetes Podcast episode 67](https://kubernetespodcast.com/episode/067-orka/) by Craig Box & Adam Glick
* [Scheduling Kubernetes workloads to Raspberry Pi using Inlets and Crossplane](https://github.com/crossplaneio/tbs/blob/master/episodes/9/assets/README.md) by [Daniel Mangum](https://github.com/hasheddan)
* Also checkout the live [demo](https://youtu.be/RVAFEAnirZA)
* Blog post by Ruan Bekker:
> Provision k3s to all the places with a awesome utility called "k3sup" by @alexellisuk. Definitely worth checking it out, its epic!
[Provision k3s on the fly with k3sup](https://sysadmins.co.za/provision-k3s-on-the-fly-with-k3sup/)
* [Dave Cadwallader (@geek_dave)](https://twitter.com/geek_dave/status/1162386683200851969?s=20):
> Alex - Thanks so much for all the effort you put into your tools and tutorials. My rpi homelab has been a valuable learning playground for CNCF tech thanks to you!
* [k3sup in KubeWeekly #181](https://kubeweekly.io/2019/08/22/kubeweekly-181/)
* [Will it cluster? K3s on Raspbian](https://blog.alexellis.io/test-drive-k3s-on-raspberry-pi/)
* [Kubernetes the Easy Way with k3sup – Cisco DevOps Series](https://blogs.cisco.com/developer/kubernetes-the-easy-way-devops-14)
* [Trying tiny k3s on Google Cloud with k3sup](https://starkandwayne.com/blog/trying-tiny-k3s-on-google-cloud-with-k3sup/) by Stark & Wayne
* [Setting up a Raspberry Pi Kubernetes Cluster with Blinkt! Strips that Show Number of Pods per Node](https://pleasereleaseme.net/setting-up-a-raspberry-pi-kubernetes-cluster-with-blinkt-strips-that-show-number-of-pods-per-node-using-k3sup/)
* [From Zero to Kubernetes Dashboard within Minutes with k3sup and Kontena Lens](https://medium.com/@laurinevala/from-zero-to-kubernetes-dashboard-within-minutes-with-k3sup-and-kontena-lens-84f881400b10) - by Lauri Nevala
* [BYOK - Build your Own Kubernetes Cluster with Raspberry Pis, k3s, and k3sup](https://speakerdeck.com/mikesir87/byok-build-your-own-kubernetes-cluster-with-raspberry-pis-k3s-and-k3sup) by Michael Irwin
* [Run Kubernetes On Your Machine with k3sup](https://itnext.io/run-kubernetes-on-your-machine-7ee463af21a2) by Luc Juggery
* [Trying out k3sup](https://blog.baeke.info/2019/10/25/trying-out-k3sup/) by Geert Baeke
* [Creating your first Kubernetes cluster with k3sup](https://dev.to/kalaspuffar/creating-your-first-kubernetes-cluster-3kp2) by Daniel Persson
* [My 2019 In Review - Hello Open Source](https://blog.heyal.co.uk/My-2019/) by Alistair Hey
* [Kubernetes 104: Create a 2-node k3s cluster with k3sup](https://ahmed-abdelsamad.blogspot.com/2019/09/kubernetes-104-create-2-node-k3s.html) by Ahmed Abelsamad
* [My home Kubernetes cluster driven by GitOps and k3sup](https://github.com/onedr0p/k3s-gitops) - by Devin Buhl
* [Raspberry Pi: From 0 to k3s cluster in 5 min with k3sup and Ansible](https://blog.cloudgsx.es/topic/10/raspberry-pi-from-0-to-k3s-cluster-in-5-min-with-k3sup-and-ansible) - by Pablo Caderno
* [Kubernetes: from Zero to Hero with Kompose, Minikube, k3sup and Helm](https://blog.mi.hdm-stuttgart.de/index.php/2020/02/29/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-2/) by Leon Klingele, Alexander Merker & Florian Wintel
* [Deploying a highly-available K3s with k3sup](https://ma.ttias.be/deploying-highly-available-k3s-k3sup/) by Dmitriy Akulov
* [Multi-master HA Kubernetes using K3Sup on Windows10/Server 2019](https://github.com/TechGuyTN/K3Sup-Windows10/blob/d7ad4f642ae6ebf441b8137bd71111c9c2890add/README.md) by Aaron Holt
* [Ansible Role: k3sup](https://github.com/vandot/ansible-role-k3sup) by Ivan Vandot
* [HashiCorp Vault on K3s on RPi4](https://github.com/colin-mccarthy/k3s-pi-vault/tree/d5e616de9048da8b990dc7b99a6d2d96bd9e9cc5) by Colin McCarthy
* [Raspberry Pi Cluster Part 2: ToDo API running on Kubernetes with k3s](https://www.dinofizzotti.com/blog/2020-05-09-raspberry-pi-cluster-part-2-todo-api-running-on-kubernetes-with-k3s/) by Dino Fizzotti
* [Cloud Native Tools for Developers Webinar Recap](https://www.openfaas.com/blog/cloud-native-tools-webinar/) by Alex Ellis & Alistair Hey
* [Unobtrusive local development with kubernetes, k3s, traefik2](https://www.codementor.io/@slavko/unobtrusive-local-development-with-kubernetes-k3s-traefik2-15uq596oja) by Vyacheslav
* [Cómo desplegar un clúster de Kubernetes en 60 segundos con k3sup](https://www.cduser.com/como-desplegar-un-cluster-de-kubernetes-en-60-segundos-con-k3sup/) by Ignacio Van Droogenbroeck
* [Raspberry Kubernetes cluster for my homlab with k3sup](https://hybridhacker.com/homelab-raspberry-kubernetes-cluster-with-k3sup.html) by Nicola Ballotta
* [k3sup On MacOS Catalina](https://gizmo.codes/k3sup-on-macos-catalina/) by John Doyle
* [Provision k3s on Google Cloud with Terraform and k3sup — Nimble](https://medium.com/nimble/provision-k3s-on-google-cloud-with-terraform-and-k3sup-nimble-38fa3167b4c3) by Carlos Herrera
* [K3s: Edge Kubernetes](https://redmonk.com/jgovernor/2020/01/31/k3s-edge-kubernetes/) by James Governor
* [OpenStack sur LXD avec Juju et k3sup dans phoenixNAP](https://deep75.medium.com/openstack-sur-lxd-avec-juju-et-k3sup-dans-phoenixnap-e5867a487497)
* [Creating a k3s Cluster with k3sup & Multipass 💻☸️](https://dev.to/tomowatt/creating-a-k3s-cluster-with-k3sup-multipass-h26) by Tom Watt
* [How I've set up my highly-available Kubernetes cluster](https://jmac.ph/2021/01/25/how-ive-set-up-my-highly-available-kubernetes-cluster/) by JJ Macalinao
* [Creating a K3S Raspberry PI Cluster with K3Sup to fire up nightscout with MongoDB](https://h3rmanns.medium.com/creating-a-k3s-raspberry-pi-cluster-with-k3sup-to-fire-up-a-nightscout-backend-service-based-on-a-27c1f5727e5b)
* [Kubernetes Cluster with Rancher on Windows using K3s](https://adyanth.site/posts/kubernetes-cluster-on-windows/) by Adyanth H
* [k3s in LXC on Proxmox with k3sup by Todd Edwards](https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185)
Checkout the [Announcement tweet](https://twitter.com/alexellisuk/status/1162272786250735618?s=20)
## Similar tools & glossary
Glossary:
* Kubernetes: master/slave
* k3s: server/agent
Related tools:
* [k3s](https://github.com/rancher/k3s) - Kubernetes as installed by `k3sup`. k3s is a compliant, light-weight, multi-architecture distribution of Kubernetes. It can be used to run Kubernetes locally or remotely for development, or in edge locations.
* [k3d](https://github.com/rancher/k3d) - this tool runs a Docker container on your local laptop with k3s inside
* [kind](https://github.com/kubernetes-sigs/kind) - kind can run a Kubernetes cluster within a Docker container for local development. k3s is also suitable for this purpose through `k3d`. KinD is not suitable for running a remote cluster for development.
* [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/) - a tool to create fully-loaded, production-ready Kubernetes clusters with or without high-availability (HA). Tends to be heavier-weight and slower than k3s. It is aimed at cloud VMs or bare-metal computers which means it doesn't always work well with low-powered ARM devices.
* [k3v](https://github.com/ibuildthecloud/k3v) - "virtual kubernetes" - a very early PoC from the author of k3s aiming to slice up a single cluster for multiple tenants
* [k3sup-multipass](https://github.com/matti/k3sup-multipass) - a helper to launch single node k3s cluster with one command using a [multipass](https://multipass.run) VM and optionally proxy the ingress to localhost for easier development.
## Troubleshooting and support
### Maybe the problem is with K3s?
If you're having issues, it's likely that this is a problem with K3s, and not with k3sup. How do we know that? K3sup is a very mature project and has a few use-cases that it generally performs very well.
Rancher provides support for K3s [on their Slack](https://slack.rancher.io/) in the `#k3s` channel. This should be your first port of call. Your second port of call is to raise an issue with the K3s maintainers in the [K3s repo](https://github.com/k3s-io/k3s/issues)
Do you want to install a specific version of K3s? See `k3sup install --help` and the `--k3s-version` and `--k3s-channel` flags.
Is your system ready to run Kubernetes? K3s requires certain Kernel modules to be available, run `k3s check-config` and check the output. Alex tests K3sup with Raspberry Pi OS and Ubuntu LTS on a regular basis.
### Common issues
The most common problem is that you missed a step, fortunately it's relatively easy to get the logs from the K3s service and it should tell you what's wrong.
* For the Raspberry Pi you probably haven't updated `cmdline.txt` to enable cgroups for CPU and memory. Update it as per the instructions in this file.
* You ran `kubectl` on a node. Don't do this. k3sup copies the file to your local workstation. Don't log into agents or servers other than to check logs / upgrade the system.
* `sudo: a terminal is required to read the password` - setup password-less `sudo` on your hosts, see also:[Pre-requisites for k3sup agents and servers](#pre-requisites-for-k3sup-servers-and-agents)
* You want to install directly on a server, without using SSH. See also: `k3sup install --local` which doesn't use SSH, but executes the commands directly on a host.
* K3s server didn't start. Log in and run `sudo systemctl status k3s` or `sudo journalctl -u k3s` to see the logs for the service.
* The K3s agent didn't start. Log in and run `sudo systemctl status k3s-agent`
* You tried to remove and re-add a server in an etcd cluster and it failed. This is a known issue, see the [K3s issue tracker](https://github.com/k3s-io/k3s/issues).
* You tried to use an unsupported version of a database for HA. See [this list from Rancher](https://rancher.com/docs/k3s/latest/en/installation/datastore/)
* Your tried to join a node to the cluster and got an error "ssh: handshake failed". This is probably one of three possibilities:
- You did not run `ssh-copy-id`. Try to run it and check if you can log in to the server and the new node without a password prompt using regular `ssh`.
- You have an RSA public key. There is an [underlying issue in a Go library](https://github.com/golang/go/issues/39885) which is [referred here](https://github.com/alexellis/k3sup/issues/63). Please provide the additional parameter `--ssh-key ~/.ssh/id_rsa` (or wherever your private key lives) until the issue is resolved.
- You are using different usernames for SSH'ing to the server and the node to be added. In that case, playe provide the username for the server via the `--server-user` parameter.
* Your `.ssh/config` file isn't being used by K3sup. K3sup does not use the config file used by the `ssh` command-line, but instead uses CLI flags, run `k3sup install/join --help` to learn which are supported.
> Note: Passing `--no-deploy` to `--k3s-extra-args` was deprecated by the K3s installer in K3s 1.17. Use `--disable` instead or `--no-extras`.
### Getting access to your KUBECONFIG
You may have run into an issue where `sudo` access is required for `kubectl` access.
You should not run `kubectl` directly on hosts where K3s is installed. k3sup is designed to rewrite and/or merge your cluster's config to your local KUBECONFIG file.
You should only run `kubectl` on your laptop / client machine.
If you've lost your kubeconfig, you can use `k3sup get-config`. See also the various flags for merging and setting a context name.
### Smart cards and 2FA
> Warning: issues requesting support for smart cards / 2FA will be closed immediately. The feature has been proven to work, and is provided as-is.
You can use a smart card or 2FA security key such as a Yubikey. You must have your ssh-agent configured correctly, at that point k3sup will defer to the agent to make connections on MacOS and Linux. [Find out more](https://github.com/alexellis/k3sup/pull/312)
### Misc note on `iptables`
> Note added by Eduardo Minguez Perez
Currently there is an issue in k3s involving `iptables >= 1.8` that can affect the network communication. See the [k3s issue](https://github.com/rancher/k3s/issues/703) and the corresponding [kubernetes one](https://github.com/kubernetes/kubernetes/issues/71305) for more information and workarounds. The issue has been observed in Debian Buster but it can affect other distributions as well.
================================================
FILE: cmd/get-config.go
================================================
package cmd
import (
"fmt"
"log"
"net"
"github.com/alexellis/k3sup/pkg"
operator "github.com/alexellis/k3sup/pkg/operator"
"github.com/spf13/cobra"
)
// MakeGetConfig creates the get-config command
func MakeGetConfig() *cobra.Command {
var command = &cobra.Command{
Use: "get-config",
Short: "Get kubeconfig from an existing K3s installation",
Long: `Create a local kubeconfig for use with kubectl from your local machine.
` + pkg.SupportMessageShort + `
`,
Example: ` # Get the kubeconfig and save it to ./kubeconfig in the local
# directory under the default context
k3sup get-config --host HOST \
--local-path ./kubeconfig
# Merge kubeconfig into local file under custom context
k3sup get-config \
--host HOST \
--merge \
--local-path $HOME/.kube/kubeconfig \
--context k3s-prod-eu-1
# Get kubeconfig from local installation directly on a server
# where you ran "k3sup install --local"
k3sup get-config --local`,
SilenceUsage: true,
}
command.Flags().IP("ip", net.ParseIP("127.0.0.1"), "Public IP of node")
command.Flags().String("user", "root", "Username for SSH login")
command.Flags().String("host", "", "Public hostname of node")
command.Flags().String("ssh-key", "~/.ssh/id_rsa", "The ssh key to use for remote login")
command.Flags().Int("ssh-port", 22, "The port on which to connect for ssh")
command.Flags().Bool("sudo", true, "Use sudo for kubeconfig retrieval. e.g. set to false when using the root user and no sudo is available.")
command.Flags().String("local-path", "kubeconfig", "Local path to save the kubeconfig file")
command.Flags().String("context", "default", "Set the name of the kubeconfig context.")
command.Flags().Bool("merge", false, `Merge the config with existing kubeconfig if it already exists.
Provide the --local-path flag with --merge if a kubeconfig already exists in some other directory`)
command.Flags().Bool("print-command", false, "Print a command that you can use with SSH to manually recover from an error")
command.Flags().Bool("local", false, "Perform a local get-config without using ssh")
command.PreRunE = func(command *cobra.Command, args []string) error {
local, err := command.Flags().GetBool("local")
if err != nil {
return err
}
if !local {
_, err = command.Flags().GetString("host")
if err != nil {
return err
}
if _, err := command.Flags().GetIP("ip"); err != nil {
return err
}
if _, err := command.Flags().GetInt("ssh-port"); err != nil {
return err
}
}
return nil
}
command.RunE = func(command *cobra.Command, args []string) error {
localKubeconfig, _ := command.Flags().GetString("local-path")
useSudo, err := command.Flags().GetBool("sudo")
if err != nil {
return err
}
sudoPrefix := ""
if useSudo {
sudoPrefix = "sudo "
}
local, _ := command.Flags().GetBool("local")
ip, err := command.Flags().GetIP("ip")
if err != nil {
return err
}
host, err := command.Flags().GetString("host")
if err != nil {
return err
}
if len(host) == 0 {
host = ip.String()
}
log.Println(host)
printCommand, err := command.Flags().GetBool("print-command")
if err != nil {
return err
}
merge, err := command.Flags().GetBool("merge")
if err != nil {
return err
}
context, err := command.Flags().GetString("context")
if err != nil {
return err
}
getConfigcommand := fmt.Sprintf("%scat /etc/rancher/k3s/k3s.yaml\n", sudoPrefix)
if local {
operator := operator.ExecOperator{}
if err = obtainKubeconfig(operator, getConfigcommand, host, context, localKubeconfig, merge); err != nil {
return err
}
return nil
}
fmt.Println("Public IP: " + host)
port, _ := command.Flags().GetInt("ssh-port")
user, _ := command.Flags().GetString("user")
sshKey, _ := command.Flags().GetString("ssh-key")
sshKeyPath := expandPath(sshKey)
address := fmt.Sprintf("%s:%d", host, port)
sshOperator, sshOperatorDone, errored, err := connectOperator(user, address, sshKeyPath)
if errored {
return err
}
if sshOperatorDone != nil {
defer sshOperatorDone()
}
if printCommand {
fmt.Printf("ssh: %s\n", getConfigcommand)
}
if err = obtainKubeconfig(sshOperator, getConfigcommand, host, context, localKubeconfig, merge); err != nil {
return err
}
return nil
}
return command
}
================================================
FILE: cmd/get.go
================================================
package cmd
import (
"github.com/alexellis/k3sup/pkg"
"github.com/spf13/cobra"
)
// MakeGet creates the get parent command
func MakeGet() *cobra.Command {
var command = &cobra.Command{
Use: "get",
Short: "Helper for downloading K3sup Pro",
Long: `Helper for downloading K3sup Pro.
` + pkg.SupportMessageShort + `
`,
SilenceUsage: true,
}
return command
}
================================================
FILE: cmd/get_pro.go
================================================
// Copyright Alex Ellis, OpenFaaS Ltd 2025
// Inspired by update command in openfaas/faas-cli
package cmd
import (
"context"
"fmt"
"io"
"os"
"path"
"runtime"
"strings"
"github.com/alexellis/arkade/pkg/archive"
"github.com/alexellis/arkade/pkg/env"
goexecute "github.com/alexellis/go-execute/v2"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/spf13/cobra"
)
// MakeGetPro creates the 'get pro' command
func MakeGetPro() *cobra.Command {
c := &cobra.Command{
Use: "pro",
Short: "Download the latest k3sup pro binary",
Long: `The latest release version of k3sup-pro will be downloaded from a remote
container registry.
This command will download and install k3sup-pro to /usr/local/bin by default.`,
Example: ` # Download to the default location
k3sup get pro
# Download a specific version of k3sup pro
k3sup get pro --version v0.10.0
# Download to a custom location
k3sup get pro --path /tmp/`,
RunE: runGetProE,
PreRunE: preRunGetProE,
}
c.Flags().Bool("verbose", false, "Enable verbose output")
c.Flags().String("path", "/usr/local/bin/", "Custom installation path")
c.Flags().String("version", "latest", "Specific version to download")
return c
}
func preRunGetProE(cmd *cobra.Command, args []string) error {
version, _ := cmd.Flags().GetString("version")
if len(version) == 0 {
return fmt.Errorf(`version must be specified, or use "latest"`)
}
return nil
}
func runGetProE(cmd *cobra.Command, args []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")
customPath, _ := cmd.Flags().GetString("path")
version, _ := cmd.Flags().GetString("version")
// Use the provided path or default to /usr/local/bin/
var binaryPath string
if customPath != "/usr/local/bin/" {
binaryPath = customPath
if verbose {
fmt.Printf("Using custom binary path: %s\n", binaryPath)
}
} else {
binaryPath = "/usr/local/bin/"
if verbose {
fmt.Printf("Using default binary path: %s\n", binaryPath)
}
}
arch, operatingSystem := getClientArch()
downloadArch, downloadOS := getDownloadArch(arch, operatingSystem)
imageRef := fmt.Sprintf("ghcr.io/openfaasltd/k3sup-pro:%s", version)
fmt.Printf("Downloading: %s (%s/%s)\n", imageRef, downloadOS, downloadArch)
tmpTarDir, err := os.MkdirTemp(os.TempDir(), "k3sup-*")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpTarDir)
tmpTar := path.Join(tmpTarDir, "k3sup-pro.tar")
f, err := os.Create(tmpTar)
if err != nil {
return fmt.Errorf("failed to open %s: %w", tmpTar, err)
}
defer f.Close()
img, err := crane.Pull(imageRef, crane.WithPlatform(&v1.Platform{Architecture: downloadArch, OS: downloadOS}))
if err != nil {
return fmt.Errorf("pulling %s: %w", imageRef, err)
}
if err := crane.Export(img, f); err != nil {
return fmt.Errorf("exporting %s: %w", imageRef, err)
}
if verbose {
fmt.Printf("Wrote OCI filesystem to: %s\n", tmpTar)
}
tarFile, err := os.Open(tmpTar)
if err != nil {
return fmt.Errorf("failed to open %s: %w", tmpTar, err)
}
defer tarFile.Close()
// Extract to temporary directory first
tmpExtractDir, err := os.MkdirTemp(os.TempDir(), "k3sup-extract-*")
if err != nil {
return fmt.Errorf("failed to create extract directory: %w", err)
}
defer os.RemoveAll(tmpExtractDir)
gzipped := false
if err := archive.Untar(tarFile, tmpExtractDir, gzipped, true); err != nil {
return fmt.Errorf("failed to untar %s: %w", tmpTar, err)
}
binaryName := "k3sup-pro"
if runtime.GOOS == "windows" {
binaryName = "k3sup-pro.exe"
}
newBinary := path.Join(tmpExtractDir, binaryName)
if err := os.Chmod(newBinary, 0755); err != nil {
return fmt.Errorf("failed to chmod %s: %w", newBinary, err)
}
// Verify the extracted binary works
if verbose {
fmt.Println("Verifying extracted binary..")
}
task := goexecute.ExecTask{
Command: newBinary,
Args: []string{"version"},
}
res, err := task.Execute(context.Background())
if err != nil {
return fmt.Errorf("failed to execute extracted binary: %w", err)
}
if res.ExitCode != 0 {
return fmt.Errorf("extracted binary test failed: %s", res.Stderr)
}
if verbose {
fmt.Printf("New binary version check:\n%s", res.Stdout)
}
// Install to target path
targetBinary := path.Join(binaryPath, binaryName)
// Ensure target directory exists
if err := os.MkdirAll(binaryPath, 0755); err != nil {
return fmt.Errorf("failed to create target directory %s: %w", binaryPath, err)
}
if err := copyFile(newBinary, targetBinary); err != nil {
return fmt.Errorf("failed to copy binary to %s: %w", targetBinary, err)
}
if err := os.Chmod(targetBinary, 0755); err != nil {
return fmt.Errorf("failed to chmod %s: %w", targetBinary, err)
}
fmt.Printf("Installed: %s.. OK.\n", targetBinary)
// Final version check
finalTask := goexecute.ExecTask{
Command: targetBinary,
Args: []string{"version"},
}
finalRes, err := finalTask.Execute(context.Background())
if err != nil {
return fmt.Errorf("failed to execute updated binary: %w", err)
}
if finalRes.ExitCode == 0 {
fmt.Println("Installation completed successfully!")
if !verbose {
fmt.Print(finalRes.Stdout)
}
}
return nil
}
func getClientArch() (arch string, os string) {
if runtime.GOOS == "windows" {
return runtime.GOARCH, runtime.GOOS
}
return env.GetClientArch()
}
func getDownloadArch(clientArch, clientOS string) (arch string, os string) {
downloadArch := strings.ToLower(clientArch)
downloadOS := strings.ToLower(clientOS)
if downloadArch == "x86_64" {
downloadArch = "amd64"
} else if downloadArch == "aarch64" {
downloadArch = "arm64"
}
return downloadArch, downloadOS
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
df, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer df.Close()
_, err = io.Copy(df, sf)
return err
}
================================================
FILE: cmd/install.go
================================================
package cmd
import (
"bytes"
"fmt"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/alexellis/k3sup/pkg"
operator "github.com/alexellis/k3sup/pkg/operator"
"errors"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/term"
)
var kubeconfig []byte
type k3sExecOptions struct {
Datastore string
Token string
ExtraArgs string
FlannelIPSec bool
NoExtras bool
}
// PinnedK3sChannel will track the stable channel of the K3s API,
// so for production use, you should pin to a specific version
// such as v1.19
// Channels API available at:
// https://update.k3s.io/v1-release/channels
const PinnedK3sChannel = "stable"
const getScript = "curl -sfL https://get.k3s.io"
// MakeInstall creates the install command
func MakeInstall() *cobra.Command {
var command = &cobra.Command{
Use: "install",
Short: "Install k3s on a server via SSH",
Long: `Install k3s on a server via SSH.
` + pkg.SupportMessageShort + `
`,
Example: ` # Simple installation of stable version, outputting a
# kubeconfig to the working directory
k3sup install --ip IP --user USER
# Merge kubeconfig into local file under custom context
k3sup install \
--host HOST \
--merge \
--local-path $HOME/.kube/kubeconfig \
--context k3s-prod-eu-1
# Only download kubeconfig
k3sup install --ip IP \
--user USER \
--skip-install
# Install a specific version on local machine without using SSH
k3sup install --local --k3s-version v1.25.1
# Install, passing extra args to K3s
k3sup install --local --k3s-extra-args="--data-dir /mnt/ssd/k3s"
# Start a cluster with embedded etcd
k3sup install --host HOST --cluster
# Install from a specific channel
k3sup install --host HOST --k3s-channel [latest|stable]
# Use a custom path to your SSH key
k3sup install --host HOST \
--ssh-key $HOME/ec2-key.pem`,
SilenceUsage: true,
}
command.Flags().IP("ip", net.ParseIP("127.0.0.1"), "Public IP of node")
command.Flags().String("user", "root", "Username for SSH login")
command.Flags().String("host", "", "Public hostname of node on which to install agent")
command.Flags().String("ssh-key", "~/.ssh/id_rsa", "The ssh key to use for remote login")
command.Flags().Int("ssh-port", 22, "The port on which to connect for ssh")
command.Flags().Bool("sudo", true, "Use sudo for installation. e.g. set to false when using the root user and no sudo is available.")
command.Flags().Bool("skip-install", false, "Skip the k3s installer")
command.Flags().String("local-path", "kubeconfig", "Local path to save the kubeconfig file")
command.Flags().String("context", "default", "Set the name of the kubeconfig context.")
command.Flags().Bool("no-extras", false, `Disable "servicelb" and "traefik"`)
command.Flags().Bool("ipsec", false, "Enforces and/or activates optional extra argument for k3s: flannel-backend option: ipsec")
command.Flags().Bool("merge", false, `Merge the config with existing kubeconfig if it already exists.
Provide the --local-path flag with --merge if a kubeconfig already exists in some other directory`)
command.Flags().Bool("local", false, "Perform a local install without using ssh")
command.Flags().Bool("cluster", false, "Form a cluster using embedded etcd (requires K8s >= 1.19)")
command.Flags().Bool("print-command", false, "Print a command that you can use with SSH to manually recover from an error")
command.Flags().String("datastore", "", "connection-string for the k3s datastore to enable HA - i.e. \"mysql://username:password@tcp(hostname:3306)/database-name\"")
command.Flags().String("token", "", "the token used to encrypt the datastore, must be the same token for all nodes")
command.Flags().String("k3s-version", "", "Set a version to install, overrides k3s-channel")
command.Flags().String("k3s-extra-args", "", "Additional arguments to pass to k3s installer, wrapped in quotes (e.g. --k3s-extra-args '--disable servicelb')")
command.Flags().String("k3s-channel", PinnedK3sChannel, "Release channel: stable, latest, or pinned v1.19")
command.Flags().String("tls-san", "", "Use an additional IP or hostname for the API server")
command.PreRunE = func(command *cobra.Command, args []string) error {
local, err := command.Flags().GetBool("local")
if err != nil {
return err
}
if !local {
_, err = command.Flags().GetString("host")
if err != nil {
return err
}
if _, err := command.Flags().GetIP("ip"); err != nil {
return err
}
if _, err := command.Flags().GetInt("ssh-port"); err != nil {
return err
}
}
return nil
}
command.RunE = func(command *cobra.Command, args []string) error {
fmt.Printf("Running: k3sup install\n")
localKubeconfig, _ := command.Flags().GetString("local-path")
skipInstall, err := command.Flags().GetBool("skip-install")
if err != nil {
return err
}
tlsSAN, _ := command.Flags().GetString("tls-san")
useSudo, err := command.Flags().GetBool("sudo")
if err != nil {
return err
}
sudoPrefix := ""
if useSudo {
sudoPrefix = "sudo "
}
k3sVersion, err := command.Flags().GetString("k3s-version")
if err != nil {
return err
}
k3sExtraArgs, err := command.Flags().GetString("k3s-extra-args")
if err != nil {
return err
}
k3sChannel, err := command.Flags().GetString("k3s-channel")
if err != nil {
return err
}
k3sNoExtras, err := command.Flags().GetBool("no-extras")
if err != nil {
return err
}
flannelIPSec, _ := command.Flags().GetBool("ipsec")
local, _ := command.Flags().GetBool("local")
ip, err := command.Flags().GetIP("ip")
if err != nil {
return err
}
host, err := command.Flags().GetString("host")
if err != nil {
return err
}
if len(host) == 0 {
host = ip.String()
}
log.Println(host)
cluster, _ := command.Flags().GetBool("cluster")
datastore, _ := command.Flags().GetString("datastore")
printCommand, err := command.Flags().GetBool("print-command")
if err != nil {
return err
}
merge, err := command.Flags().GetBool("merge")
if err != nil {
return err
}
context, err := command.Flags().GetString("context")
if err != nil {
return err
}
token, err := command.Flags().GetString("token")
if err != nil {
return err
}
if len(datastore) > 0 {
if strings.Index(datastore, "ssl-mode=REQUIRED") > -1 {
return fmt.Errorf("remove ssl-mode=REQUIRED from your datastore string, it is not supported by the k3s syntax")
}
if strings.Index(datastore, "mysql") > -1 && strings.Index(datastore, "tcp") == -1 {
return fmt.Errorf("you must specify the mysql host as tcp(host:port) or tcp(ip:port), see the k3s docs for more: https://rancher.com/docs/k3s/latest/en/installation/ha")
}
if token == "" {
return fmt.Errorf("you must provide the token when using an external datastore. Make sure to use the same token as other nodes")
}
}
installk3sExec := makeInstallExec(cluster, host, tlsSAN,
k3sExecOptions{
Datastore: datastore,
Token: token,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
if len(k3sVersion) == 0 && len(k3sChannel) == 0 {
return fmt.Errorf("give a value for --k3s-version or --k3s-channel")
}
installStr := createVersionStr(k3sVersion, k3sChannel)
installK3scommand := fmt.Sprintf("%s | %s %s sh -\n", getScript, installk3sExec, installStr)
getConfigcommand := fmt.Sprintf("%scat /etc/rancher/k3s/k3s.yaml\n", sudoPrefix)
if local {
operator := operator.ExecOperator{}
if !skipInstall {
fmt.Printf("Executing: %s\n", installK3scommand)
res, err := operator.Execute(installK3scommand)
if err != nil {
return err
}
if res.ExitCode != 0 {
if len(res.StdErr) > 0 {
fmt.Printf("stderr: %q", res.StdErr)
}
}
if len(res.StdOut) > 0 {
fmt.Printf("stdout: %q", res.StdOut)
}
} else {
fmt.Printf("Skipping local installation\n")
}
if err = obtainKubeconfig(operator, getConfigcommand, host, context, localKubeconfig, merge); err != nil {
return err
}
return nil
}
fmt.Println("Public IP: " + host)
port, _ := command.Flags().GetInt("ssh-port")
user, _ := command.Flags().GetString("user")
sshKey, _ := command.Flags().GetString("ssh-key")
sshKeyPath := expandPath(sshKey)
address := fmt.Sprintf("%s:%d", host, port)
sshOperator, sshOperatorDone, errored, err := connectOperator(user, address, sshKeyPath)
if errored {
return err
}
if sshOperatorDone != nil {
defer sshOperatorDone()
}
if !skipInstall {
if printCommand {
fmt.Printf("ssh: %s\n", installK3scommand)
}
res, err := sshOperator.Execute(installK3scommand)
if err != nil {
return fmt.Errorf("error received processing command: %s", err)
}
fmt.Printf("Result: %s %s\n", string(res.StdOut), string(res.StdErr))
}
if printCommand {
fmt.Printf("ssh: %s\n", getConfigcommand)
}
if err = obtainKubeconfig(sshOperator, getConfigcommand, host, context, localKubeconfig, merge); err != nil {
return err
}
return nil
}
return command
}
type DoneFunc func()
// connectOperator
//
// Try SSH agent without parsing key files, will succeed if the user
// has already added a key to the SSH Agent, or if using a configured
// smartcard.
//
// If the initial connection attempt fails fall through to the using
// the supplied/default private key file
// DoneFunc should be called by the caller to close the SSH connection when done
func connectOperator(user string, address string, sshKeyPath string) (*operator.SSHOperator, DoneFunc, bool, error) {
var sshOperator *operator.SSHOperator
var initialSSHErr error
var closeSSHAgentFunc func() error
doneFunc := func() {
if sshOperator != nil {
sshOperator.Close()
}
if closeSSHAgentFunc != nil {
closeSSHAgentFunc()
}
}
if runtime.GOOS != "windows" {
var sshAgentAuthMethod ssh.AuthMethod
sshAgentAuthMethod, initialSSHErr = sshAgentOnly()
if initialSSHErr == nil {
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{sshAgentAuthMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, initialSSHErr = operator.NewSSHOperator(address, config)
}
} else {
initialSSHErr = errors.New("ssh-agent unsupported on windows")
}
if initialSSHErr != nil {
publicKeyFileAuth, closeSSHAgent, err := loadPublickey(sshKeyPath)
if err != nil {
return nil, nil, true, fmt.Errorf("unable to load the ssh key with path %q: %w", sshKeyPath, err)
}
defer closeSSHAgent()
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{publicKeyFileAuth},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, err = operator.NewSSHOperator(address, config)
if err != nil {
return nil, nil, true, fmt.Errorf("unable to connect to %s over ssh: %w", address, err)
}
}
return sshOperator, doneFunc, false, nil
}
func sshAgentOnly() (ssh.AuthMethod, error) {
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return nil, err
}
return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers), nil
}
func obtainKubeconfig(operator operator.CommandOperator, getConfigcommand, host, context, localKubeconfig string, merge bool) error {
res, err := operator.ExecuteStdio(getConfigcommand, false)
if err != nil {
return fmt.Errorf("error received processing command: %s", err)
}
absPath, _ := filepath.Abs(expandPath(localKubeconfig))
kubeconfig := rewriteKubeconfig(string(res.StdOut), host, context)
if merge {
// Create a merged kubeconfig
kubeconfig, err = mergeConfigs(absPath, context, []byte(kubeconfig))
if err != nil {
return err
}
}
// Create a new kubeconfig
if err := writeConfig(absPath, []byte(kubeconfig), context, false); err != nil {
return err
}
return nil
}
// Generates config files give the path to file: string and the data: []byte
func writeConfig(path string, data []byte, context string, suppressMessage bool) error {
absPath, _ := filepath.Abs(path)
if !suppressMessage {
fmt.Printf(`Saving file to: %s
# Test your cluster with:
export KUBECONFIG=%s
kubectl config use-context %s
kubectl get node -o wide
%s
`,
absPath,
absPath,
context,
pkg.SupportMessageShort)
}
if err := os.WriteFile(absPath, []byte(data), 0600); err != nil {
return err
}
return nil
}
func mergeConfigs(localKubeconfigPath, context string, k3sconfig []byte) ([]byte, error) {
// Create a temporary kubeconfig to store the config of the newly create k3s cluster
file, err := os.CreateTemp(os.TempDir(), "k3s-temp-*")
if err != nil {
return nil, fmt.Errorf("could not generate a temporary file to store the kubeconfig: %w", err)
}
defer func() {
// Remove the temporarily generated file, even if there is an error and the
// function returns early
if err = os.Remove(file.Name()); err != nil {
log.Printf("could not remove temporary kubeconfig file: %s %s", file.Name(), err)
}
}()
if err := writeConfig(file.Name(), []byte(k3sconfig), context, true); err != nil {
return nil, err
}
fmt.Printf("Merging config into file: %s\n", localKubeconfigPath)
// Pick between ; or : for path concatenation
var joinChar string
if runtime.GOOS == "windows" {
joinChar = ";"
} else {
joinChar = ":"
}
appendKubeConfigENV := fmt.Sprintf("KUBECONFIG=%s%s%s",
localKubeconfigPath,
joinChar,
file.Name())
// Merge the two kubeconfigs and read the output into 'data'
cmd := exec.Command("kubectl", "config", "view", "--merge", "--flatten")
cmd.Env = append(os.Environ(), appendKubeConfigENV)
data, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("could not merge kubeconfig: %w", err)
}
if err := file.Close(); err != nil {
return nil, fmt.Errorf("could not close temporary kubeconfig file: %s %w",
file.Name(), err)
}
return data, nil
}
func expandPath(path string) string {
res, _ := homedir.Expand(path)
return res
}
func sshAgent(publicKeyPath string) (ssh.AuthMethod, func() error) {
if sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
sshAgent := agent.NewClient(sshAgentConn)
keys, _ := sshAgent.List()
if len(keys) == 0 {
return nil, sshAgentConn.Close
}
pubkey, err := os.ReadFile(publicKeyPath)
if err != nil {
return nil, sshAgentConn.Close
}
authkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey)
if err != nil {
return nil, sshAgentConn.Close
}
parsedkey := authkey.Marshal()
for _, key := range keys {
if bytes.Equal(key.Blob, parsedkey) {
return ssh.PublicKeysCallback(sshAgent.Signers), sshAgentConn.Close
}
}
}
return nil, func() error { return nil }
}
func loadPublickey(path string) (ssh.AuthMethod, func() error, error) {
noopCloseFunc := func() error { return nil }
key, err := os.ReadFile(path)
if err != nil {
return nil, noopCloseFunc, fmt.Errorf("unable to read file: %s, %s", path, err)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
return nil, noopCloseFunc, fmt.Errorf("unable to parse private key: %s", err.Error())
}
agent, close := sshAgent(path + ".pub")
if agent != nil {
return agent, close, nil
}
defer close()
fmt.Printf("Enter passphrase for '%s': ", path)
STDIN := int(os.Stdin.Fd())
bytePassword, _ := term.ReadPassword(STDIN)
// Ignore any error from reading stdin to retain existing behaviour for unit test in
// install_test.go
// if err != nil {
// return nil, noopCloseFunc, fmt.Errorf("reading password from stdin failed: %s", err.Error())
// }
fmt.Println()
signer, err = ssh.ParsePrivateKeyWithPassphrase(key, bytePassword)
if err != nil {
return nil, noopCloseFunc, fmt.Errorf("parse private key with passphrase failed: %s", err)
}
}
return ssh.PublicKeys(signer), noopCloseFunc, nil
}
// rewriteKubeconfig replaces the IP address of the server with the IP address
// it also changes the context from "default" to the value of the --context flag
func rewriteKubeconfig(kubeconfig string, host string, context string) []byte {
if context == "" {
context = "default"
}
kubeconfigReplacer := strings.NewReplacer(
"127.0.0.1", host,
"localhost", host,
"default", context,
)
return []byte(kubeconfigReplacer.Replace(kubeconfig))
}
func makeInstallExec(cluster bool, host, tlsSAN string, options k3sExecOptions) string {
extraArgs := []string{}
if len(options.Datastore) > 0 {
extraArgs = append(extraArgs, fmt.Sprintf("--datastore-endpoint %s", options.Datastore))
extraArgs = append(extraArgs, fmt.Sprintf("--token %s", options.Token))
}
if options.FlannelIPSec {
extraArgs = append(extraArgs, "--flannel-backend ipsec")
}
if options.NoExtras {
extraArgs = append(extraArgs, "--disable servicelb")
extraArgs = append(extraArgs, "--disable traefik")
}
extraArgs = append(extraArgs, options.ExtraArgs)
extraArgsCmdline := ""
for _, a := range extraArgs {
extraArgsCmdline += a + " "
}
installExec := "INSTALL_K3S_EXEC='server"
if cluster {
installExec += " --cluster-init"
}
san := host
if len(tlsSAN) > 0 {
san = tlsSAN
}
installExec += fmt.Sprintf(" --tls-san %s", san)
if trimmed := strings.TrimSpace(extraArgsCmdline); len(trimmed) > 0 {
installExec += fmt.Sprintf(" %s", trimmed)
}
installExec += "'"
return installExec
}
================================================
FILE: cmd/install_test.go
================================================
package cmd
import (
"errors"
"os"
"regexp"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
// To regenerate:
// openssl genrsa -des3 -out /tmp/id_rsa_encrypted 2048
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,4FA02C824DA7DA35
0MxJJqh8FGEciV4fkZzq7bCfKmPy5a3x9eJ+8sY+ssNGG8cLMdV2uDPMrarssjBK
QtaEFUMu2f2lxuXYvPzZtSQNkcUUj2kBJCxgdrs7mGLgqnLOYkbkWA3rUiiNYf2S
UofpsAO4gVWcQ4HBtnWW6skQp4fa0fdfg8elKlOrM0wcRX890attpyCCbfEAtn/v
6z31ezObnGQOFSZ9kM7icbAM8pPgjex3kno5kxzVfpyL+5pq36AyFoFBN1wzYzMf
gpwtr/m+Kw/KUWVGFKXLKgFSe9aF/dIXisdVCJze+2uQEBDXZo1OiJ3rflrTKwlB
t2NNLPdd23MOHK8B8dBWhitppBqloy68Thfw0cFq2E4qtUIl9SqtBhcQ118E0Z/7
UGVg6Ki+sgBO5fHcQUnDn7DGV8/Gawl+ZOhvGkD9C2Q/vK/SaKEg3X7ap4Oo2/em
NGVVnxCpgd3GfVHsZFHjRvt/YYQtBHdAhH8cU45WlRaLbUyKSyfq7TIAzDa04bl+
VMytKkfwYoPG8E3POABZ2lOgDWeBQeK3eP7EkxTkv3sSahWIwFE1HaBZmhoL27vH
necstfLEHqKkONvaXzqSbKk7e0GXKooSgZS2NJP7wJSX4e5CbOBrM4hf69CIG5bm
rPPYs9mhxsa+iP4X5EVxdr1IEUTzwqeLB+/e/C/+mbs37L7tv3yvKC8UG8gXr1jC
qzm+V3SSH9W5tgqDx97ljuDqLXgZl158W5NbYIwXB7FazU1DEJAOGSgu21w8XqlF
SmxKXJHAjLwGzkygNGZYRGllq8GppZxLeUZHmlL+F490BclIdxCaOir//Bqd+a8a
bs3Q7D57kuo003x9z0e044anmANdmEFSjfPG7ajHUfm7EqsQ4pZOYp4twllOJkY5
Yffoe94wdYbMGtrBKY9xeZPgecDZpjMv1g6pB5Gt6p4VLz/U2rstTkjqiHTZfIej
tphVIzOTpsfVNMG4As3WOapz+9MH2kzEKORAHpQpZenyvcAfhJJa404riZ3HJ++O
Nmc7ASSirGNty1BTJKKQtN/QDvVbM011jUpuQxbEwfUDAUlQU4g5YElfMw3l9tDo
jWM4jimYxGaeaTI2C6hjy7pLMWCywkOGrKVKuii8EI8vd4Mw9jTIMRQzBotzEBFn
qAy3PMlnGd/CDs/HPAWqPWEloU9bcY8oP954EEfNZoNz95u6VMJkqfM/ynu1yBEl
FjG6pf31NEqjYTeFmJROozLGLxdPTrchn/MYU60oG/eJfY+eZ02h8J58yC67aG/f
7tCUfB8UrQH1s16BY2j9EM6KPbX3Hh8VXiKb7/UzIPtD9aD5HKzl7K3fIbi+aQcX
ySQXENXiPpieDZj7kKp9VskNjpLyXyR1BN7Tf3eIZ6N1gK1d6esZMhXhPR5S4LT9
F8ZD5KeHVWB6hOaodWr/bhfEVb8E67/OcnLQM8iKdBfqkoPDInVIXGkt8FXQfiB0
I+rSXfppnf7bhQK3HLeU27Ca6zxQYZ7TI6bXTRBjozFakKkQ+8xcfCVzZ/0/oZgu
kfFJfrUjElq6Bx9oPPxc2vD40gqnYL57A+Y+X+A0kL4fO7pfh2VxOw==
-----END RSA PRIVATE KEY-----
`
func Test_loadPublickeyEncrypted(t *testing.T) {
want := &ssh.PassphraseMissingError{}
tmpfile, err := os.CreateTemp("", "key")
if err != nil {
t.Error(err)
}
fileName := tmpfile.Name()
defer os.Remove(fileName)
if _, err := tmpfile.Write([]byte(privateKey)); err != nil {
t.Fatalf("unable to write test file %s, %s", fileName, err)
}
tmpfile.Close()
_, _, err = loadPublickey(fileName)
if errors.Is(err, want) {
t.Fatalf("want: %q, but got: %q", want, err.Error())
}
}
const kubeconfigExample = `
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://localhost:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
password: 5ceb3a3e93621d265fd147929f3ace84
username: admin
`
func Test_RewriteKubeconfig(t *testing.T) {
var ip = "192.168.0.25"
var context = "context-test"
// Test master ip rewrite
kubeconfig = rewriteKubeconfig(kubeconfigExample, ip, context)
re := regexp.MustCompile(`server:\s?https://(.*):\d+`)
group := re.FindSubmatch(kubeconfig)
if len(group) == 0 || string(group[1]) != ip {
t.Fatalf("unexpected error, got: %q, want: %q.", string(group[1]), ip)
}
kubeconfigExampleIPLocal := strings.Replace(kubeconfigExample, "localhost", "127.0.0.1", -1)
kubeconfig = rewriteKubeconfig(kubeconfigExampleIPLocal, ip, context)
group = re.FindSubmatch(kubeconfig)
if len(group) == 0 || string(group[1]) != ip {
t.Fatalf("unexpected error, got: %q, want: %q.", string(group[1]), ip)
}
// Test context
re = regexp.MustCompile(`default`)
expectedContextsToReplace := re.FindAllStringIndex(kubeconfigExample, -1)
kubeconfig = rewriteKubeconfig(kubeconfigExample, ip, "")
match := re.FindAllIndex(kubeconfig, -1)
if len(match) != len(expectedContextsToReplace) {
t.Fatalf("unexpected error, got: %q, want: %q.", len(match), len(expectedContextsToReplace))
}
kubeconfig = rewriteKubeconfig(kubeconfigExample, ip, context)
re = regexp.MustCompile(`context-test`)
match = re.FindAllIndex(kubeconfig, -1)
if len(match) != len(expectedContextsToReplace) {
t.Fatalf("unexpected error, got: %q, want: %q.", len(match), len(expectedContextsToReplace))
}
}
func Test_makeInstallExec(t *testing.T) {
cluster := false
datastore := ""
flannelIPSec := false
k3sNoExtras := false
k3sExtraArgs := ""
ip := "raspberrypi.local"
tlsSAN := ""
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --tls-san raspberrypi.local'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
func Test_makeInstallExec_Cluster(t *testing.T) {
cluster := true
datastore := ""
flannelIPSec := false
k3sNoExtras := false
k3sExtraArgs := ""
ip := "127.0.0.1"
tlsSAN := ""
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --cluster-init --tls-san 127.0.0.1'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
func Test_makeInstallExec_SAN(t *testing.T) {
cluster := false
datastore := ""
flannelIPSec := false
k3sNoExtras := false
k3sExtraArgs := ""
ip := "127.0.0.1"
tlsSAN := "192.168.0.1"
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --tls-san 192.168.0.1'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
func Test_makeInstallExec_IPSec(t *testing.T) {
cluster := false
datastore := ""
flannelIPSec := true
k3sNoExtras := false
k3sExtraArgs := ""
ip := "127.0.0.1"
tlsSAN := ""
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --tls-san 127.0.0.1 --flannel-backend ipsec'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
func Test_makeInstallExec_Datastore(t *testing.T) {
cluster := false
datastore := "mysql://doadmin:show-password@tcp(db-mysql-lon1-40939-do-user-2197152-0.b.db.ondigitalocean.com:25060)/defaultdb"
flannelIPSec := false
k3sNoExtras := false
k3sExtraArgs := ""
token := "this-token"
ip := "127.0.0.1"
tlsSAN := "192.168.0.1"
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
Token: token,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --tls-san 192.168.0.1 --datastore-endpoint mysql://doadmin:show-password@tcp(db-mysql-lon1-40939-do-user-2197152-0.b.db.ondigitalocean.com:25060)/defaultdb --token this-token'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
func Test_makeInstallExec_Datastore_NoExtras(t *testing.T) {
cluster := false
datastore := "mysql://doadmin:show-password@tcp(db-mysql-lon1-40939-do-user-2197152-0.b.db.ondigitalocean.com:25060)/defaultdb"
flannelIPSec := false
k3sNoExtras := true
token := "this-token"
k3sExtraArgs := ""
ip := "raspberrypi.local"
tlsSAN := "192.168.0.1"
got := makeInstallExec(cluster, ip, tlsSAN,
k3sExecOptions{
Datastore: datastore,
Token: token,
FlannelIPSec: flannelIPSec,
NoExtras: k3sNoExtras,
ExtraArgs: k3sExtraArgs,
})
want := "INSTALL_K3S_EXEC='server --tls-san 192.168.0.1 --datastore-endpoint mysql://doadmin:show-password@tcp(db-mysql-lon1-40939-do-user-2197152-0.b.db.ondigitalocean.com:25060)/defaultdb --token this-token --disable servicelb --disable traefik'"
if got != want {
t.Errorf("want: %q, got: %q", want, got)
}
}
================================================
FILE: cmd/join.go
================================================
package cmd
import (
"fmt"
"net"
"os"
"path"
"runtime"
"strings"
"errors"
"github.com/alexellis/k3sup/pkg"
operator "github.com/alexellis/k3sup/pkg/operator"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)
// MakeJoin creates the join command
func MakeJoin() *cobra.Command {
var command = &cobra.Command{
Use: "join",
Short: "Install the k3s agent on a remote host and join it to an existing server",
Long: `Install the k3s agent on a remote host and join it to an existing server
` + pkg.SupportMessageShort + `
`,
Example: ` # Install K3s joining a cluster as an agent
k3sup join \
--user AGENT_USER \
--ip AGENT_IP \
--server-ip IP \
--server-user SERVER_USER
# Install K3s joining a cluster as another server
k3sup join \
--user AGENT_USER \
--ip AGENT_IP \
--server \
--server-ip IP \
--server-user SERVER_USER
# Join whilst specifying a channel for the k3sup version
k3sup join --user pi \
--server-host HOST \
--host HOST \
--k3s-channel latest`,
SilenceUsage: true,
}
command.Flags().IP("ip", net.ParseIP("127.0.0.1"), "Public IP of node on which to install agent")
command.Flags().IP("server-ip", net.ParseIP("127.0.0.1"), "Public IP of an existing k3s server")
command.Flags().String("host", "", "Public hostname of node on which to install agent")
command.Flags().String("server-host", "", "Public hostname of an existing k3s server")
command.Flags().String("server-url", "", "If different from server-ip or server-host, the URL of the server to join")
command.Flags().String("user", "root", "Username for SSH login")
command.Flags().String("server-user", "root", "Server username for SSH login (Default to --user)")
command.Flags().String("ssh-key", "~/.ssh/id_rsa", "The ssh key to use for remote login")
command.Flags().Int("ssh-port", 22, "The port on which to connect for ssh")
command.Flags().Int("server-ssh-port", 22, "The port on which to connect to server for ssh (Default to --ssh-port)")
command.Flags().Bool("skip-install", false, "Skip the k3s installer")
command.Flags().Bool("sudo", true, "Use sudo for installation. e.g. set to false when using the root user and no sudo is available.")
command.Flags().Bool("server", false, "Join the cluster as a server rather than as an agent for the embedded etcd mode")
command.Flags().Bool("no-extras", false, `Disable "servicelb" and "traefik", when using --server flag`)
command.Flags().Bool("print-command", false, "Print a command that you can use with SSH to manually recover from an error")
command.Flags().String("node-token-path", "", "file containing --node-token")
command.Flags().String("node-token", "", "prefetched token used by nodes to join the cluster")
command.Flags().String("k3s-extra-args", "", "Additional arguments to pass to k3s installer, wrapped in quotes (e.g. --k3s-extra-args '--node-taint key=value:NoExecute')")
command.Flags().String("k3s-version", "", "Set a version to install, overrides k3s-channel")
command.Flags().String("k3s-channel", PinnedK3sChannel, "Release channel: stable, latest, or i.e. v1.19")
command.Flags().String("tls-san", "", "Use an additional IP or hostname for the API server, when using --server flag")
command.Flags().String("server-data-dir", "/var/lib/rancher/k3s/", "Override the path used to fetch the node-token from the server")
command.RunE = func(command *cobra.Command, args []string) error {
fmt.Printf("Running: k3sup join\n")
ip, err := command.Flags().GetIP("ip")
if err != nil {
return err
}
var nodeToken string
if command.Flags().Changed("node-token") {
nodeToken, _ = command.Flags().GetString("node-token")
} else if command.Flags().Changed("node-token-path") {
nodeTokenPath, _ := command.Flags().GetString("node-token-path")
if len(nodeTokenPath) > 0 {
data, err := os.ReadFile(nodeTokenPath)
if err != nil {
return err
}
nodeToken = strings.TrimSpace(string(data))
}
}
host, err := command.Flags().GetString("host")
if err != nil {
return err
}
if len(host) == 0 {
host = ip.String()
}
dataDir, err := command.Flags().GetString("server-data-dir")
if err != nil {
return err
}
if len(dataDir) == 0 {
return fmt.Errorf("--server-data-dir must be set")
}
if !strings.HasPrefix(dataDir, "/") {
return fmt.Errorf("--server-data-dir must begin with /")
}
serverIP, err := command.Flags().GetIP("server-ip")
if err != nil {
return err
}
serverHost, err := command.Flags().GetString("server-host")
if err != nil {
return err
}
if len(serverHost) == 0 {
serverHost = serverIP.String()
}
serverURL, err := command.Flags().GetString("server-url")
if err != nil {
return err
}
fmt.Printf("Joining %s => %s\n", host, serverHost)
if len(serverURL) > 0 {
fmt.Printf("Server join URL: %s\n", serverURL)
}
user, _ := command.Flags().GetString("user")
serverUser := user
if command.Flags().Changed("server-user") {
serverUser, _ = command.Flags().GetString("server-user")
}
sshKey, _ := command.Flags().GetString("ssh-key")
server, err := command.Flags().GetBool("server")
if err != nil {
return err
}
port, _ := command.Flags().GetInt("ssh-port")
serverPort := port
if command.Flags().Changed("server-ssh-port") {
serverPort, _ = command.Flags().GetInt("server-ssh-port")
}
k3sVersion, err := command.Flags().GetString("k3s-version")
if err != nil {
return err
}
k3sExtraArgs, err := command.Flags().GetString("k3s-extra-args")
if err != nil {
return err
}
k3sChannel, err := command.Flags().GetString("k3s-channel")
if err != nil {
return err
}
if len(k3sVersion) == 0 && len(k3sChannel) == 0 {
return fmt.Errorf("give a value for --k3s-version or --k3s-channel")
}
printCommand, err := command.Flags().GetBool("print-command")
if err != nil {
return err
}
useSudo, err := command.Flags().GetBool("sudo")
if err != nil {
return err
}
sudoPrefix := ""
if useSudo {
sudoPrefix = "sudo "
}
sshKeyPath := expandPath(sshKey)
if len(nodeToken) == 0 {
address := fmt.Sprintf("%s:%d", serverHost, serverPort)
sshOperator, sshOperatorDone, errored, err := connectOperator(serverUser, address, sshKeyPath)
if errored {
return err
}
if sshOperatorDone != nil {
defer sshOperatorDone()
}
getTokenCommand := fmt.Sprintf("%scat %s\n", sudoPrefix, path.Join(dataDir, "/server/node-token"))
if printCommand {
fmt.Printf("ssh: %s\n", getTokenCommand)
}
streamToStdio := false
res, err := sshOperator.ExecuteStdio(getTokenCommand, streamToStdio)
if err != nil {
return fmt.Errorf("unable to get join-token from server: %w", err)
}
if len(res.StdErr) > 0 {
fmt.Printf("Error or warning getting node-token: %s\n", res.StdErr)
} else {
fmt.Printf("Received node-token from %s.. ok.\n", serverHost)
}
// Explicit close of the SSH connection as early as possible
// which complements the defer
if sshOperatorDone != nil {
sshOperatorDone()
}
nodeToken = strings.TrimSpace(string(res.StdOut))
}
if server {
tlsSan, _ := command.Flags().GetString("tls-san")
noExtras, _ := command.Flags().GetBool("no-extras")
err = setupAdditionalServer(serverHost, host, port, user, sshKeyPath, nodeToken, k3sExtraArgs, k3sVersion, k3sChannel, tlsSan, printCommand, serverURL, noExtras)
} else {
err = setupAgent(serverHost, host, port, user, sshKeyPath, nodeToken, k3sExtraArgs, k3sVersion, k3sChannel, printCommand, serverURL)
}
if err == nil {
fmt.Printf("\n%s\n", pkg.SupportMessageShort)
}
return err
}
command.PreRunE = func(command *cobra.Command, args []string) error {
_, err := command.Flags().GetIP("ip")
if err != nil {
return err
}
_, err = command.Flags().GetIP("server-ip")
if err != nil {
return err
}
_, err = command.Flags().GetString("host")
if err != nil {
return err
}
_, err = command.Flags().GetString("server-host")
if err != nil {
return err
}
_, err = command.Flags().GetInt("ssh-port")
if err != nil {
return err
}
tlsSan, err := command.Flags().GetString("tls-san")
if err != nil {
return err
}
noExtras, err := command.Flags().GetBool("no-extras")
if err != nil {
return err
}
if len(tlsSan) > 0 || noExtras {
server, err := command.Flags().GetBool("server")
if err != nil {
return err
}
if !server {
if noExtras {
return fmt.Errorf("--no-extras can only be used with --server")
}
return fmt.Errorf("--tls-san can only be used with --server")
}
}
return nil
}
return command
}
func setupAdditionalServer(serverHost, host string, port int, user, sshKeyPath, joinToken, k3sExtraArgs, k3sVersion, k3sChannel, tlsSAN string, printCommand bool, serverURL string, noExtras bool) error {
address := fmt.Sprintf("%s:%d", host, port)
var sshOperator *operator.SSHOperator
var initialSSHErr error
if runtime.GOOS != "windows" {
var sshAgentAuthMethod ssh.AuthMethod
sshAgentAuthMethod, initialSSHErr = sshAgentOnly()
if initialSSHErr == nil {
// Try SSH agent without parsing key files, will succeed if the user
// has already added a key to the SSH Agent, or if using a configured
// smartcard
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{sshAgentAuthMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, initialSSHErr = operator.NewSSHOperator(address, config)
}
} else {
initialSSHErr = errors.New("ssh-agent unsupported on windows")
}
// If the initial connection attempt fails fall through to the using
// the supplied/default private key file
if initialSSHErr != nil {
publicKeyFileAuth, closeSSHAgent, err := loadPublickey(sshKeyPath)
if err != nil {
return fmt.Errorf("unable to load the ssh key with path %q: %w", sshKeyPath, err)
}
defer closeSSHAgent()
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
publicKeyFileAuth,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, err = operator.NewSSHOperator(address, config)
if err != nil {
return fmt.Errorf("unable to connect to %s over ssh as %s: %w", address, user, err)
}
}
installStr := createVersionStr(k3sVersion, k3sChannel)
serverAgent := true
defer sshOperator.Close()
if noExtras {
k3sExtraArgs += " --disable servicelb"
k3sExtraArgs += " --disable traefik"
}
installk3sExec := makeJoinExec(
serverHost,
strings.TrimSpace(joinToken),
installStr,
k3sExtraArgs,
serverAgent,
serverURL,
tlsSAN,
)
installAgentServerCommand := fmt.Sprintf("%s | %s", getScript, installk3sExec)
if printCommand {
fmt.Printf("ssh: %s\n", installAgentServerCommand)
}
res, err := sshOperator.Execute(installAgentServerCommand)
if err != nil {
return fmt.Errorf("unable to setup agent: %w", err)
}
if len(res.StdErr) > 0 {
fmt.Printf("Logs: %s", res.StdErr)
}
joinRes := string(res.StdOut)
fmt.Printf("Output: %s", string(joinRes))
return nil
}
func setupAgent(serverHost, host string, port int, user, sshKeyPath, joinToken, k3sExtraArgs, k3sVersion, k3sChannel string, printCommand bool, serverURL string) error {
address := fmt.Sprintf("%s:%d", host, port)
var sshOperator *operator.SSHOperator
var initialSSHErr error
if runtime.GOOS != "windows" {
var sshAgentAuthMethod ssh.AuthMethod
sshAgentAuthMethod, initialSSHErr = sshAgentOnly()
if initialSSHErr == nil {
// Try SSH agent without parsing key files, will succeed if the user
// has already added a key to the SSH Agent, or if using a configured
// smartcard
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{sshAgentAuthMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, initialSSHErr = operator.NewSSHOperator(address, config)
}
} else {
initialSSHErr = errors.New("ssh-agent unsupported on windows")
}
// If the initial connection attempt fails fall through to the using
// the supplied/default private key file
if initialSSHErr != nil {
publicKeyFileAuth, closeSSHAgent, err := loadPublickey(sshKeyPath)
if err != nil {
return fmt.Errorf("unable to load the ssh key with path %q: %w", sshKeyPath, err)
}
defer closeSSHAgent()
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
publicKeyFileAuth,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshOperator, err = operator.NewSSHOperator(address, config)
if err != nil {
return fmt.Errorf("unable to connect to %s over ssh: %w", address, err)
}
}
defer sshOperator.Close()
installStr := createVersionStr(k3sVersion, k3sChannel)
serverAgent := false
// Agents don't expose an API server so don't need a TLS SAN
tlsSAN := ""
installK3sExec := makeJoinExec(
serverHost,
strings.TrimSpace(joinToken),
installStr,
k3sExtraArgs,
serverAgent,
serverURL,
tlsSAN,
)
installAgentCommand := fmt.Sprintf("%s | %s", getScript, installK3sExec)
if printCommand {
fmt.Printf("ssh: %s\n", installAgentCommand)
}
res, err := sshOperator.Execute(installAgentCommand)
if err != nil {
return fmt.Errorf("unable to setup agent: %w", err)
}
if len(res.StdErr) > 0 {
fmt.Printf("Logs: %s", res.StdErr)
}
joinRes := string(res.StdOut)
fmt.Printf("Output: %s", string(joinRes))
return nil
}
func createVersionStr(k3sVersion, k3sChannel string) string {
installStr := ""
if len(k3sVersion) > 0 {
installStr = fmt.Sprintf("INSTALL_K3S_VERSION='%s'", k3sVersion)
} else {
installStr = fmt.Sprintf("INSTALL_K3S_CHANNEL='%s'", k3sChannel)
}
return installStr
}
func makeJoinExec(serverIP, joinToken, installStr, k3sExtraArgs string, serverAgent bool, serverURL, tlsSan string) string {
installEnvVar := []string{}
remoteURL := fmt.Sprintf("https://%s:6443", serverIP)
if len(serverURL) > 0 {
remoteURL = serverURL
}
installEnvVar = append(installEnvVar, fmt.Sprintf("K3S_URL='%s'", remoteURL))
installEnvVar = append(installEnvVar, fmt.Sprintf("K3S_TOKEN='%s'", joinToken))
installEnvVar = append(installEnvVar, installStr)
if serverAgent {
tlsSANValue := ""
if len(tlsSan) > 0 {
tlsSANValue = fmt.Sprintf(" --tls-san %s", tlsSan)
}
installEnvVar = append(installEnvVar, fmt.Sprintf("INSTALL_K3S_EXEC='server --server %s%s'", remoteURL, tlsSANValue))
}
joinExec := strings.Join(installEnvVar, " ")
joinExec += " sh -s -"
if len(k3sExtraArgs) > 0 {
// AE: this doesn't seem to be used
// installEnvVar = append(installEnvVar, k3sExtraArgs)
joinExec += fmt.Sprintf(" %s", k3sExtraArgs)
}
return joinExec
}
================================================
FILE: cmd/join_test.go
================================================
package cmd
import (
"testing"
)
type test struct {
title string
serverIP string
joinToken string
installStr string
k3sExtraArgs string
serverAgent bool
installk3sExec string
tlsSAN string
}
func Test_makeJoinServerExec(t *testing.T) {
tests := []test{
{
title: "Join Server without k3sExtraArgs",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 INSTALL_K3S_EXEC='server --server https://172.27.251.164:6443' sh -s -",
k3sExtraArgs: "",
serverAgent: true,
},
{
title: "Join Server with K3sExtraArgs",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 INSTALL_K3S_EXEC='server --server https://172.27.251.164:6443' sh -s - --node-taint key=value:NoExecute",
k3sExtraArgs: "--node-taint key=value:NoExecute",
serverAgent: true,
},
{
title: "Join Server with K3sExtraArgs and TLS SAN",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 INSTALL_K3S_EXEC='server --server https://172.27.251.164:6443 --tls-san 127.0.0.1' sh -s - --node-taint key=value:NoExecute",
k3sExtraArgs: "--node-taint key=value:NoExecute",
serverAgent: true,
tlsSAN: "127.0.0.1",
},
{
title: "Join agent with K3sExtraArgs",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 sh -s - --node-ip=192.0.3.4 --node-external-ip=85.159.215.50",
k3sExtraArgs: "--node-ip=192.0.3.4 --node-external-ip=85.159.215.50",
serverAgent: false,
tlsSAN: "127.0.0.1",
},
}
for _, tc := range tests {
t.Run(tc.title, func(t *testing.T) {
got := makeJoinExec(tc.serverIP, tc.joinToken, tc.installStr, tc.k3sExtraArgs, tc.serverAgent, "", tc.tlsSAN)
if got != tc.installk3sExec {
t.Errorf("want:\n%s\n, got:\n%s\n", tc.installk3sExec, got)
}
})
}
}
func Test_makeJoinAgentExec(t *testing.T) {
tests := []test{
{
title: "Join Agent without K3sExtraArgs",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
k3sExtraArgs: "",
serverAgent: false,
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 sh -s -",
},
{
title: "Join Agent with K3sExtraArgs",
serverIP: "172.27.251.164",
joinToken: "K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d",
installStr: "INSTALL_K3S_VERSION=1.18",
installk3sExec: "K3S_URL='https://172.27.251.164:6443' K3S_TOKEN='K10c8bc21f68fef3f56d431a08df2e894481ab0a61a3c84cbd639b56449ad15523c::server:9d30861e1ba54177b8e4dd1426076e5d' INSTALL_K3S_VERSION=1.18 sh -s - --node-taint key=value:NoExecute",
k3sExtraArgs: "--node-taint key=value:NoExecute",
serverAgent: false,
},
}
for _, tc := range tests {
t.Run(tc.title, func(t *testing.T) {
got := makeJoinExec(tc.serverIP, tc.joinToken, tc.installStr, tc.k3sExtraArgs, tc.serverAgent, "", "")
if got != tc.installk3sExec {
t.Errorf("want: %s, got: %s", tc.installk3sExec, got)
}
})
}
}
================================================
FILE: cmd/node-token.go
================================================
package cmd
import (
"fmt"
"net"
"os"
"path"
"strings"
"github.com/alexellis/k3sup/pkg"
ssh "github.com/alexellis/k3sup/pkg/operator"
"github.com/spf13/cobra"
)
// MakeNodeToken creates the node-token command
func MakeNodeToken() *cobra.Command {
var command = &cobra.Command{
Use: "node-token",
Short: "Retrieve the node token from a server",
Long: `Retrieve the node token from a server required for a
server or agent to join the cluster.
` + pkg.SupportMessageShort + `
`,
Example: ` # Get the node token from the server and pipe it to a file
k3sup node-token --ip IP --user USER > token.txt
`,
SilenceUsage: true,
}
command.Flags().IP("ip", net.ParseIP("127.0.0.1"), "Public IP of node")
command.Flags().String("user", "root", "Username for SSH login")
command.Flags().String("host", "", "Public hostname of node on which to install agent")
command.Flags().Bool("local", false, "Use local machine instead of ssh client")
command.Flags().String("ssh-key", "~/.ssh/id_rsa", "The ssh key to use for remote login")
command.Flags().Int("ssh-port", 22, "The port on which to connect for ssh")
command.Flags().Bool("sudo", true, "Use sudo for installation. e.g. set to false when using the root user and no sudo is available.")
command.Flags().Bool("print-command", false, "Print the command to be executed")
command.Flags().String("server-data-dir", "/var/lib/rancher/k3s/", "Override the path used to fetch the node-token from the server")
command.PreRunE = func(command *cobra.Command, args []string) error {
local, err := command.Flags().GetBool("local")
if err != nil {
return err
}
if !local {
_, err = command.Flags().GetString("host")
if err != nil {
return err
}
if _, err := command.Flags().GetIP("ip"); err != nil {
return err
}
if _, err := command.Flags().GetInt("ssh-port"); err != nil {
return err
}
}
return nil
}
command.RunE = func(command *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "Fetching: /etc/rancher/k3s/k3s.yaml\n")
useSudo, err := command.Flags().GetBool("sudo")
if err != nil {
return err
}
sudoPrefix := ""
if useSudo {
sudoPrefix = "sudo "
}
local, _ := command.Flags().GetBool("local")
ip, err := command.Flags().GetIP("ip")
if err != nil {
return err
}
host, err := command.Flags().GetString("host")
if err != nil {
return err
}
if len(host) == 0 {
host = ip.String()
}
port, _ := command.Flags().GetInt("ssh-port")
user, _ := command.Flags().GetString("user")
sshKey, _ := command.Flags().GetString("ssh-key")
dataDir, _ := command.Flags().GetString("server-data-dir")
sshKeyPath := expandPath(sshKey)
address := fmt.Sprintf("%s:%d", host, port)
if !local {
fmt.Fprintf(os.Stderr, "Remote: %s\n", address)
}
printCommand := false
getTokenCommand := fmt.Sprintf("%scat %s\n", sudoPrefix, path.Join(dataDir, "/server/node-token"))
if printCommand {
fmt.Printf("ssh: %s\n", getTokenCommand)
}
var operator ssh.CommandOperator
if local {
operator = ssh.ExecOperator{}
} else {
sshOperator, sshOperatorDone, errored, err := connectOperator(user, address, sshKeyPath)
if errored {
return err
}
operator = sshOperator
if sshOperatorDone != nil {
defer sshOperatorDone()
}
}
nodeToken, err := obtainNodeToken(operator, getTokenCommand, host)
if err != nil {
return err
}
if len(nodeToken) == 0 {
return fmt.Errorf("no node token found")
}
fmt.Println(nodeToken)
return nil
}
return command
}
func obtainNodeToken(operator ssh.CommandOperator, command, host string) (string, error) {
res, err := operator.ExecuteStdio(command, false)
if err != nil {
return "", fmt.Errorf("error received processing command: %s", err)
}
return strings.TrimSpace(string(res.StdOut)), nil
}
================================================
FILE: cmd/plan.go
================================================
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/alexellis/k3sup/pkg"
"github.com/spf13/cobra"
)
func MakePlan() *cobra.Command {
var command = &cobra.Command{
Use: "plan",
Short: "Plan an installation of K3s.",
Long: `Generate a bash script or plan of installation commands for K3s for a
Highly Available (HA) Kubernetes cluster.
Examples JSON input file:
[{"hostname": "node-1", "ip": "192.168.128.102"},
{"hostname": "node-2", "ip": "192.168.128.103"},
{"hostname": "node-3", "ip": "192.168.128.104"}]
` + pkg.SupportMessageShort + `
`,
Example: ` # Generate an installation script where the first
# 3 available hosts are dedicated as servers, with a custom user.
# The remaining hosts are added as agents.
k3sup plan hosts.json --servers 3 --user ubuntu
# Override the TLS SAN, for HA with 5 servers specified
k3sup plan hosts.json --servers 5 --tls-san $SAN_IP
`,
SilenceUsage: true,
}
command.Flags().Int("servers", 3, "Number of servers to use from the devices file")
command.Flags().String("local-path", "kubeconfig", "Where to save the kubeconfig file")
command.Flags().String("context", "default", "Name of the kubeconfig context to use")
command.Flags().String("user", "root", "Username for SSH login")
command.Flags().String("ssh-key", "", "Path to the private key for SSH login")
command.Flags().String("tls-san", "", "SAN for TLS certificates, can be a comma-separated list")
command.Flags().String("server-k3s-extra-args", "", "Extra arguments to be passed into the k3s server")
command.Flags().String("agent-k3s-extra-args", "", "Extra arguments to be passed into the k3s agent")
// Background
command.Flags().Bool("background", false, "Run the installation in the background for all agents/nodes after the first server is up")
command.Flags().Int("limit", 0, "Maximum number of nodes to use from the devices file, 0 to use all devices")
command.Flags().Bool("merge", true, `Merge the config with existing kubeconfig if it already exists.
Provide the --local-path flag with --merge if a kubeconfig already exists in some other directory`)
command.RunE = func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("give a path to a JSON file containing a list of devices")
}
nodeLimit, _ := cmd.Flags().GetInt("limit")
name := args[0]
data, err := os.ReadFile(name)
if err != nil {
return err
}
background, _ := cmd.Flags().GetBool("background")
merge, _ := cmd.Flags().GetBool("merge")
var hosts []Host
if err = json.Unmarshal(data, &hosts); err != nil {
return err
}
serverK3sExtraArgs, _ := cmd.Flags().GetString("server-k3s-extra-args")
agentK3sExtraArgs, _ := cmd.Flags().GetString("agent-k3s-extra-args")
servers, _ := cmd.Flags().GetInt("servers")
kubeconfig, _ := cmd.Flags().GetString("local-path")
contextName, _ := cmd.Flags().GetString("context")
user, _ := cmd.Flags().GetString("user")
tlsSan, _ := cmd.Flags().GetString("tls-san")
tlsSanStr := ""
if len(tlsSan) > 0 {
tlsSanStr = fmt.Sprintf(` \
--tls-san %s`, tlsSan)
}
sshKey, _ := cmd.Flags().GetString("ssh-key")
sshKeySt := ""
if len(sshKey) > 0 {
sshKeySt = fmt.Sprintf(` \
--ssh-key %s`, sshKey)
}
mergeStr := ""
if merge {
if _, err := os.Stat(kubeconfig); err == nil {
mergeStr = " \n--merge"
}
}
bgStr := ""
if background {
bgStr = " &"
}
serversAdded := 0
var primaryServer Host
script := "#!/bin/sh\n\n"
serverExtraArgsSt := ""
if len(serverK3sExtraArgs) > 0 {
serverExtraArgsSt = fmt.Sprintf(` \
--k3s-extra-args "%s"`, serverK3sExtraArgs)
}
agentExtraArgsSt := ""
if len(agentK3sExtraArgs) > 0 {
agentExtraArgsSt = fmt.Sprintf(` \
--k3s-extra-args "%s"`, agentK3sExtraArgs)
}
for i, host := range hosts {
if serversAdded == 0 {
script += `echo "Setting up primary server 1"
`
script += fmt.Sprintf(`k3sup install --host %s \
--user %s \
--cluster \
--local-path %s \
--context %s%s%s%s%s
`,
host.IP,
user,
kubeconfig,
contextName,
tlsSanStr,
serverExtraArgsSt,
sshKeySt,
mergeStr)
script += fmt.Sprintf(`
echo "Fetching the server's node-token into memory"
export NODE_TOKEN=$(k3sup node-token --host %s --user %s%s)
`, host.IP, user, sshKeySt)
serversAdded = 1
primaryServer = host
} else if serversAdded < servers {
script += fmt.Sprintf("\necho \"Setting up additional server: %d\"\n", serversAdded+1)
script += fmt.Sprintf(`k3sup join \
--host %s \
--server-host %s \
--server \
--node-token "$NODE_TOKEN" \
--user %s%s%s%s%s
`, host.IP, primaryServer.IP, user, tlsSanStr, serverExtraArgsSt, sshKeySt, bgStr)
serversAdded++
} else {
script += fmt.Sprintf("\necho \"Setting up worker: %d\"\n", (i+1)-serversAdded)
script += fmt.Sprintf(`k3sup join \
--host %s \
--server-host %s \
--node-token "$NODE_TOKEN" \
--user %s%s%s%s
`, host.IP, primaryServer.IP, user, agentExtraArgsSt, sshKeySt, bgStr)
}
if nodeLimit > 0 && i+1 >= nodeLimit {
break
}
}
fmt.Printf("%s\n", script)
return nil
}
return command
}
type Host struct {
Hostname string `json:"hostname"`
IP string `json:"ip"`
}
================================================
FILE: cmd/pro.go
================================================
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func MakePro() *cobra.Command {
var command = &cobra.Command{
Use: "pro",
Short: "Learn about K3sup Pro",
Long: `K3sup Pro is built for professionals, teams, and homelabs:
- IaaC/GitOps workflow with plan and apply commands
- Parallel installation across many nodes
- Rolling upgrades and day-2 operations
- Uninstall, exec, and get-config across your fleet
- Pre-download K3s binaries for efficient installations across many nodes
- Integrates directly with https://slicervm.com
Learn more at https://github.com/alexellis/k3sup#k3sup-pro`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(cmd.Long)
},
}
return command
}
================================================
FILE: cmd/ready.go
================================================
package cmd
import (
"fmt"
"os"
"strings"
"time"
execute "github.com/alexellis/go-execute/v2"
"github.com/alexellis/k3sup/pkg"
"github.com/spf13/cobra"
)
func MakeReady() *cobra.Command {
var command = &cobra.Command{
Use: "ready",
Short: "Check if a cluster is ready using kubectl.",
Long: `Check if the K3s cluster is ready using kubectl to query the nodes.
` + pkg.SupportMessageShort + `
`,
Example: ` # Check from a local file, with the context "default"
k3sup ready \
--context default \
--kubeconfig ./kubeconfig
# Check a merged kubeconfig with a custom context
k3sup ready \
--context e2e \
--kubeconfig $HOME/.kube/config
`,
SilenceUsage: true,
}
command.Flags().Int("attempts", 25, "Number of attempts to check for readiness")
command.Flags().Duration("pause", time.Second*2, "Pause between checking cluster for readiness")
command.Flags().String("kubeconfig", "$HOME/.kube/config", "Path to the kubeconfig file")
command.Flags().String("context", "default", "Name of the kubeconfig context to use")
command.Flags().Bool("quiet", false, "Suppress output from each attempt")
command.RunE = func(cmd *cobra.Command, args []string) error {
attempts, _ := cmd.Flags().GetInt("attempts")
pause, _ := cmd.Flags().GetDuration("pause")
kubeconfig, _ := cmd.Flags().GetString("kubeconfig")
contextName, _ := cmd.Flags().GetString("context")
quiet, _ := cmd.Flags().GetBool("quiet")
if len(kubeconfig) == 0 {
return fmt.Errorf("kubeconfig cannot be empty")
}
if len(contextName) == 0 {
return fmt.Errorf("context cannot be empty")
}
kubeconfig = os.ExpandEnv(kubeconfig)
// Inspired by Kind: https://github.com/kubernetes-sigs/kind/blob/main/pkg/cluster/internal/create/actions/waitforready/waitforready.go
for i := 0; i < attempts; i++ {
if !quiet {
fmt.Printf("Checking for nodes to be ready: %d/%d \n", i+1, attempts)
}
task := execute.ExecTask{
Command: "kubectl",
Args: []string{
"get",
"nodes",
"--kubeconfig=" + kubeconfig,
"--context=" + contextName,
"-o=jsonpath='{.items..status.conditions[-1:].status}'",
},
StreamStdio: false,
}
res, err := task.Execute(cmd.Context())
if err != nil {
return err
}
if strings.Contains(res.Stderr, "context was not found") {
return fmt.Errorf("context %s not found in %s", contextName, kubeconfig)
}
if res.ExitCode == 0 {
parts := strings.Split(strings.TrimSpace(res.Stdout), " ")
ready := true
for _, part := range parts {
trimmed := strings.TrimSpace(part)
trimmed = strings.Trim(trimmed, "'")
// Note: The command is returning a single quoted string
if len(trimmed) > 0 && trimmed != "True" {
ready = false
break
}
}
if ready {
if !quiet {
fmt.Printf("All node(s) are ready\n")
}
break
}
}
time.Sleep(pause)
}
// Wait until the default service account is created. This was causing a failure during CI.
for i := 0; i < attempts; i++ {
if !quiet {
fmt.Printf("Looking for default service account: %d/%d \n", i+1, attempts)
}
task := execute.ExecTask{
Command: "kubectl",
Args: []string{
"get",
"serviceaccount",
"default",
"--kubeconfig=" + kubeconfig,
"--context=" + contextName,
},
StreamStdio: false,
}
res, err := task.Execute(cmd.Context())
if err != nil {
return err
}
if res.ExitCode == 0 {
if !quiet {
fmt.Printf("Default service account is ready\n")
}
break
}
time.Sleep(pause)
}
return nil
}
return command
}
================================================
FILE: cmd/update.go
================================================
package cmd
import (
"fmt"
"github.com/alexellis/k3sup/pkg"
"github.com/spf13/cobra"
)
func MakeUpdate() *cobra.Command {
var command = &cobra.Command{
Use: "update",
Short: "Print update instructions",
Long: `Print instructions for updating your version of k3sup.
` + pkg.SupportMessageShort + `
`,
Example: ` k3sup update`,
SilenceUsage: false,
}
command.Run = func(cmd *cobra.Command, args []string) {
fmt.Println(k3supUpdate)
}
return command
}
const k3supUpdate = `You can update k3sup with the following:
# Use arkade, for a quick installation:
arkade get k3sup
# Remove cached versions of tools
rm -rf $HOME/.k3sup
# For Linux/MacOS:
curl -SLfs https://get.k3sup.dev | sudo sh
# For Windows (using Git Bash)
curl -SLfs https://get.k3sup.dev | sh
# Or download from GitHub: https://github.com/alexellis/k3sup/releases
` + pkg.SupportMessageShort
================================================
FILE: cmd/version.go
================================================
package cmd
import (
"fmt"
"github.com/alexellis/k3sup/pkg"
"github.com/morikuni/aec"
"github.com/spf13/cobra"
)
var (
Version string
GitCommit string
)
func PrintK3supASCIIArt() {
k3supLogo := aec.GreenF.Apply(k3supFigletStr)
fmt.Print(k3supLogo)
}
func MakeVersion() *cobra.Command {
var command = &cobra.Command{
Use: "version",
Short: "Print the version",
Example: ` k3sup version
` + pkg.SupportMessageShort + `
`,
SilenceUsage: false,
}
command.Run = func(cmd *cobra.Command, args []string) {
PrintK3supASCIIArt()
if len(Version) == 0 {
fmt.Println("Version: dev")
} else {
fmt.Println("Version:", Version)
}
fmt.Println("Git Commit:", GitCommit)
}
return command
}
const k3supFigletStr = ` _ _____ ____ _____
| | _|___ / ___ _ _ _ __ / ___| ____|
| |/ / |_ \/ __| | | | '_ \ | | | _|
| < ___) \__ \ |_| | |_) | | |___| |___
|_|\_\____/|___/\__,_| .__/ \____|_____|
|_|
bootstrap K3s over SSH in < 60s 🚀
`
================================================
FILE: docs/assets/README.md
================================================
The assets in this directory are not licensed under the MIT license. Please contact support@openfaas.com if you would like to use them.
================================================
FILE: get.sh
================================================
#!/bin/bash
# Copyright OpenFaaS Author(s) 2019
#########################
# Repo specific content #
#########################
export VERIFY_CHECKSUM=0
export ALIAS=""
export OWNER=alexellis
export REPO=k3sup
export BINLOCATION="/usr/local/bin"
export SUCCESS_CMD="$BINLOCATION/$REPO version"
GET_PRO=false
if [ "$PRO" = "true" ]; then
GET_PRO=true
fi
if [ "$PRO" = "1" ]; then
GET_PRO=true
fi
if [ "$GET_PRO" = "true" ]; then
echo "PRO=true"
echo ""
echo "Plan: download K3sup CE then upgrade to K3sup Pro"
echo ""
fi
###############################
# Content common across repos #
###############################
version=$(curl -sI https://github.com/$OWNER/$REPO/releases/latest | grep -i "location:" | awk -F"/" '{ printf "%s", $NF }' | tr -d '\r')
if [ ! $version ]; then
echo "Failed while attempting to install $REPO. Please manually install:"
echo ""
echo "1. Open your web browser and go to https://github.com/$OWNER/$REPO/releases"
echo "2. Download the latest release for your platform. Call it '$REPO'."
echo "3. chmod +x ./$REPO"
echo "4. mv ./$REPO $BINLOCATION"
if [ -n "$ALIAS_NAME" ]; then
echo "5. ln -sf $BINLOCATION/$REPO $BINLOCATION/$ALIAS_NAME"
fi
exit 1
fi
hasCli() {
hasCurl=$(which curl)
if [ "$?" = "1" ]; then
echo "You need curl to use this script."
exit 1
fi
}
checkHash(){
sha_cmd="sha256sum"
if [ ! -x "$(command -v $sha_cmd)" ]; then
sha_cmd="shasum -a 256"
fi
if [ -x "$(command -v $sha_cmd)" ]; then
targetFileDir=${targetFile%/*}
(cd $targetFileDir && curl -sSL $url.sha256|$sha_cmd -c >/dev/null)
if [ "$?" != "0" ]; then
rm $targetFile
echo "Binary checksum didn't match. Exiting"
exit 1
fi
fi
}
getPackage() {
uname=$(uname)
userid=$(id -u)
suffix=""
case $uname in
"Darwin")
arch=$(uname -m)
echo $arch
case $arch in
"x86_64")
suffix="-darwin"
;;
esac
case $arch in
"arm64")
suffix="-darwin-arm64"
;;
esac
;;
"MINGW"*)
suffix=".exe"
BINLOCATION="$HOME/bin"
mkdir -p $BINLOCATION
;;
"Linux")
arch=$(uname -m)
echo $arch
case $arch in
"aarch64")
suffix="-arm64"
;;
esac
case $arch in
"armv6l" | "armv7l")
suffix="-armhf"
;;
esac
;;
esac
targetFile="/tmp/$REPO$suffix"
if [ "$userid" != "0" ]; then
targetFile="$(pwd)/$REPO$suffix"
fi
if [ -e "$targetFile" ]; then
rm "$targetFile"
fi
url=https://github.com/$OWNER/$REPO/releases/download/$version/$REPO$suffix
echo "Downloading package $url as $targetFile"
curl -sSL $url --output "$targetFile"
if [ "$?" = "0" ]; then
if [ "$VERIFY_CHECKSUM" = "1" ]; then
checkHash
fi
chmod +x "$targetFile"
echo "Download complete."
if [ ! -w "$BINLOCATION" ]; then
echo
echo "============================================================"
echo " The script was run as a user who is unable to write"
echo " to $BINLOCATION. To complete the installation the"
echo " following commands may need to be run manually."
echo "============================================================"
echo
echo " sudo cp $REPO$suffix $BINLOCATION/$REPO"
if [ -n "$ALIAS_NAME" ]; then
echo " sudo ln -sf $BINLOCATION/$REPO $BINLOCATION/$ALIAS_NAME"
fi
echo
else
echo
echo "Running with sufficient permissions to attempt to move $REPO to $BINLOCATION"
if [ ! -w "$BINLOCATION/$REPO" ] && [ -f "$BINLOCATION/$REPO" ]; then
echo
echo "================================================================"
echo " $BINLOCATION/$REPO already exists and is not writeable"
echo " by the current user. Please adjust the binary ownership"
echo " or run sh/bash with sudo."
echo "================================================================"
echo
exit 1
fi
mv $targetFile $BINLOCATION/$REPO
if [ "$?" = "0" ]; then
echo "New version of $REPO installed to $BINLOCATION"
fi
if [ -e "$targetFile" ]; then
rm "$targetFile"
fi
if [ -n "$ALIAS_NAME" ]; then
if [ ! -L $BINLOCATION/$ALIAS_NAME ]; then
ln -s $BINLOCATION/$REPO $BINLOCATION/$ALIAS_NAME
echo "Creating alias '$ALIAS_NAME' for '$REPO'."
fi
fi
${SUCCESS_CMD}
fi
fi
}
thanks() {
echo
echo "================================================================"
echo " Tip: Create clusters on Mac, Linux + WSL2 with K3sup"
echo ""
echo " https://slicervm.com"
echo "================================================================"
echo
}
hasCli
getPackage
if [ "$GET_PRO" = "false" ]; then
thanks
else
echo "Upgrading to K3sup Pro"
$BINLOCATION/$REPO get pro
fi
================================================
FILE: go.mod
================================================
module github.com/alexellis/k3sup
go 1.25.6
require (
github.com/alexellis/arkade v0.0.0-20260304131458-e29d4142a4d5
github.com/alexellis/go-execute/v2 v2.2.1
github.com/google/go-containerregistry v0.21.2
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.1.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.48.0
golang.org/x/term v0.40.0
)
require (
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/alexellis/arkade v0.0.0-20250818095951-5f1a8a0d86df h1:2G+jZF9Wn1o8abf70sIKWWeOvhEoBDgzJ0bxWtECHno=
github.com/alexellis/arkade v0.0.0-20250818095951-5f1a8a0d86df/go.mod h1:dDqIrbXxrj6BDvQIehG14v1ZqBZQJ5PIVffa+pYXM8M=
github.com/alexellis/arkade v0.0.0-20260304131458-e29d4142a4d5 h1:cY1I3s7+xf6uy5Zrw5QHNuJ5pse1uszX76exlchoSQI=
github.com/alexellis/arkade v0.0.0-20260304131458-e29d4142a4d5/go.mod h1:LyWSdkHrM/UJADQTNy8h2IEIi6GN8ouhUL/XDuzcRI0=
github.com/alexellis/go-execute/v2 v2.2.1 h1:4Ye3jiCKQarstODOEmqDSRCqxMHLkC92Bhse743RdOI=
github.com/alexellis/go-execute/v2 v2.2.1/go.mod h1:FMdRnUTiFAmYXcv23txrp3VYZfLo24nMpiIneWgKHTQ=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0=
github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: hack/hashgen.sh
================================================
#!/bin/sh
for f in bin/k3sup*; do shasum -a 256 $f > $f.sha256; done
================================================
FILE: hack/platform-tag.sh
================================================
#!/bin/sh
getPackage() {
suffix=""
arch=$(uname -m)
case $arch in
"aarch64")
suffix="-arm64"
;;
esac
case $arch in
"armv6l" | "armv7l")
suffix="-armhf"
;;
esac
}
getPackage
echo ${suffix}
================================================
FILE: main.go
================================================
package main
import (
"os"
"github.com/alexellis/k3sup/cmd"
"github.com/alexellis/k3sup/pkg"
"github.com/spf13/cobra"
)
func main() {
cmdInstall := cmd.MakeInstall()
cmdVersion := cmd.MakeVersion()
cmdJoin := cmd.MakeJoin()
cmdUpdate := cmd.MakeUpdate()
cmdReady := cmd.MakeReady()
cmdPlan := cmd.MakePlan()
cmdNodeToken := cmd.MakeNodeToken()
cmdGetConfig := cmd.MakeGetConfig()
cmdGet := cmd.MakeGet()
cmdGetPro := cmd.MakeGetPro()
cmdPro := cmd.MakePro()
printk3supASCIIArt := cmd.PrintK3supASCIIArt
var rootCmd = &cobra.Command{
Use: "k3sup",
Run: func(cmd *cobra.Command, args []string) {
printk3supASCIIArt()
cmd.Help()
},
Long: pkg.SupportMessageShort,
}
rootCmd.AddCommand(cmdInstall)
rootCmd.AddCommand(cmdVersion)
rootCmd.AddCommand(cmdJoin)
rootCmd.AddCommand(cmdUpdate)
rootCmd.AddCommand(cmdReady)
rootCmd.AddCommand(cmdPlan)
rootCmd.AddCommand(cmdNodeToken)
rootCmd.AddCommand(cmdGetConfig)
cmdGet.AddCommand(cmdGetPro)
rootCmd.AddCommand(cmdGet)
rootCmd.AddCommand(cmdPro)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
================================================
FILE: pkg/operator/exec_operator.go
================================================
package ssh
import (
"context"
goexecute "github.com/alexellis/go-execute/v2"
)
// ExecOperator executes commands on the local machine directly
type ExecOperator struct {
}
func (ex ExecOperator) ExecuteStdio(command string, stream bool) (CommandRes, error) {
task := goexecute.ExecTask{
Command: command,
Shell: true,
StreamStdio: stream,
}
res, err := task.Execute(context.Background())
if err != nil {
return CommandRes{}, err
}
return CommandRes{
StdErr: []byte(res.Stderr),
StdOut: []byte(res.Stdout),
ExitCode: res.ExitCode,
}, nil
}
func (ex ExecOperator) Execute(command string) (CommandRes, error) {
return ex.ExecuteStdio(command, true)
}
================================================
FILE: pkg/operator/operator.go
================================================
package ssh
// CommandOperator executes a command on a machine to install k3sup
type CommandOperator interface {
Execute(command string) (CommandRes, error)
ExecuteStdio(command string, stream bool) (CommandRes, error)
}
// CommandRes contains the STDIO output from running a command
type CommandRes struct {
StdOut []byte
StdErr []byte
ExitCode int
}
================================================
FILE: pkg/operator/ssh_operator.go
================================================
package ssh
import (
"bytes"
"io"
"os"
"sync"
"golang.org/x/crypto/ssh"
)
// SSHOperator executes commands on a remote machine over an SSH session
type SSHOperator struct {
conn *ssh.Client
}
func NewSSHOperator(address string, config *ssh.ClientConfig) (*SSHOperator, error) {
conn, err := ssh.Dial("tcp", address, config)
if err != nil {
return nil, err
}
operator := SSHOperator{
conn: conn,
}
return &operator, nil
}
func (s SSHOperator) ExecuteStdio(command string, stream bool) (CommandRes, error) {
sess, err := s.conn.NewSession()
if err != nil {
return CommandRes{}, err
}
defer sess.Close()
sessStdOut, err := sess.StdoutPipe()
if err != nil {
return CommandRes{}, err
}
output := bytes.Buffer{}
wg := sync.WaitGroup{}
var stdOutWriter io.Writer
if stream {
stdOutWriter = io.MultiWriter(os.Stdout, &output)
} else {
stdOutWriter = &output
}
wg.Add(1)
go func() {
io.Copy(stdOutWriter, sessStdOut)
wg.Done()
}()
sessStderr, err := sess.StderrPipe()
if err != nil {
return CommandRes{}, err
}
errorOutput := bytes.Buffer{}
var stdErrWriter io.Writer
if stream {
stdErrWriter = io.MultiWriter(os.Stderr, &errorOutput)
} else {
stdErrWriter = &errorOutput
}
wg.Add(1)
go func() {
io.Copy(stdErrWriter, sessStderr)
wg.Done()
}()
err = sess.Run(command)
if err != nil {
return CommandRes{}, err
}
wg.Wait()
return CommandRes{
StdErr: errorOutput.Bytes(),
StdOut: output.Bytes(),
}, nil
}
func (s SSHOperator) Execute(command string) (CommandRes, error) {
return s.ExecuteStdio(command, true)
}
func (s SSHOperator) Close() error {
return s.conn.Close()
}
================================================
FILE: pkg/thanks.go
================================================
package pkg
// SupportMessageShort is a tip shown to users after commands.
const SupportMessageShort = `Tip: Create clusters on Mac, Linux + WSL2 with K3sup and https://slicervm.com`
================================================
FILE: vendor/github.com/alexellis/arkade/LICENSE
================================================
MIT License
Copyright (c) 2019 Alex Ellis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: vendor/github.com/alexellis/arkade/pkg/archive/untar.go
================================================
package archive
import (
"archive/tar"
gzipc "compress/gzip"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
)
// Untar reads the (gzip-compressed) tar file from r and writes it into dir.
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// AE: Edited on 2019-10-11 to remove support for nested folders when un-taring
// so that all files are placed in the same target directory
func Untar(r io.Reader, dir string, gzip bool, quiet bool) error {
return untar(r, dir, gzip, quiet)
}
func untar(r io.Reader, dir string, gzip bool, quiet bool) (err error) {
t0 := time.Now()
nFiles := 0
madeDir := map[string]bool{}
defer func() {
td := time.Since(t0)
if err == nil {
if !quiet {
log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
}
} else {
log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
}
}()
if gzip {
r, err = gzipc.NewReader(r)
if err != nil {
return fmt.Errorf("requires gzip-compressed body: %v", err)
}
}
tr := tar.NewReader(r)
loggedChtimesError := false
for {
f, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Printf("tar reading error: %v", err)
return fmt.Errorf("tar error: %v", err)
}
if !validRelPath(f.Name) {
return fmt.Errorf("tar contained invalid name error %q", f.Name)
}
baseFile := filepath.Base(f.Name)
abs := path.Join(dir, baseFile)
if !quiet {
fmt.Printf("Extracting: %s to\t%s\n", f.Name, abs)
}
fi := f.FileInfo()
mode := fi.Mode()
switch {
case mode.IsDir():
break
case mode.IsRegular():
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
if err != nil {
return err
}
n, err := io.Copy(wf, tr)
if closeErr := wf.Close(); closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
return fmt.Errorf("error writing to %s: %v", abs, err)
}
if n != f.Size {
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
}
modTime := f.ModTime
if modTime.After(t0) {
// Clamp modtimes at system time. See
// golang.org/issue/19062 when clock on
// buildlet was behind the gitmirror server
// doing the git-archive.
modTime = t0
}
if !modTime.IsZero() {
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
// benign error. Gerrit doesn't even set the
// modtime in these, and we don't end up relying
// on it anywhere (the gomote push command relies
// on digests only), so this is a little pointless
// for now.
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
loggedChtimesError = true // once is enough
}
}
nFiles++
default:
}
}
return nil
}
func validRelPath(p string) bool {
if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
return false
}
return true
}
================================================
FILE: vendor/github.com/alexellis/arkade/pkg/archive/untar_nested.go
================================================
package archive
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
// UntarNested reads the gzip-compressed tar file from r and writes it into dir.
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
func UntarNested(r io.Reader, dir string, gzipped, quiet bool) error {
return untarNested(r, dir, gzipped, quiet)
}
func untarNested(r io.Reader, dir string, gzipped, quiet bool) (err error) {
t0 := time.Now()
nFiles := 0
madeDir := map[string]bool{}
defer func() {
td := time.Since(t0)
if err == nil {
if !quiet {
log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
}
} else {
log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
}
}()
reader := r
if gzipped {
zr, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("requires gzip-compressed body: %v", err)
}
reader = zr
}
tr := tar.NewReader(reader)
loggedChtimesError := false
for {
f, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Printf("tar reading error: %v", err)
return fmt.Errorf("tar error: %v", err)
}
if !validRelPath(f.Name) {
return fmt.Errorf("tar contained invalid name error %q", f.Name)
}
rel := filepath.FromSlash(f.Name)
abs := filepath.Join(dir, rel)
fi := f.FileInfo()
mode := fi.Mode()
if !quiet {
fmt.Printf("Extracting: %s\n", abs)
}
switch {
case mode.IsRegular():
// Make the directory. This is redundant because it should
// already be made by a directory entry in the tar
// beforehand. Thus, don't check for errors; the next
// write will fail with the same error.
dir := filepath.Dir(abs)
if !madeDir[dir] {
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
return err
}
madeDir[dir] = true
}
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
if err != nil {
return err
}
n, err := io.Copy(wf, tr)
if closeErr := wf.Close(); closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
return fmt.Errorf("error writing to %s: %v", abs, err)
}
if n != f.Size {
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
}
modTime := f.ModTime
if modTime.After(t0) {
// Clamp modtimes at system time. See
// golang.org/issue/19062 when clock on
// buildlet was behind the gitmirror server
// doing the git-archive.
modTime = t0
}
if !modTime.IsZero() {
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
// benign error. Gerrit doesn't even set the
// modtime in these, and we don't end up relying
// on it anywhere (the gomote push command relies
// on digests only), so this is a little pointless
// for now.
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
loggedChtimesError = true // once is enough
}
}
nFiles++
case mode.IsDir():
if err := os.MkdirAll(abs, 0755); err != nil {
return err
}
madeDir[abs] = true
// Introduced via
// https://github.com/alexellis/arkade/pull/675/files
case os.ModeSymlink != 0:
if err := os.Symlink(f.Linkname, abs); err != nil {
return err
}
default:
return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
}
}
return nil
}
================================================
FILE: vendor/github.com/alexellis/arkade/pkg/archive/unzip.go
================================================
// Copyright (c) arkade author(s) 2022. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package archive
import (
"archive/zip"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"time"
)
// Unzip reads the compressed zip file from reader and writes it into dir.
// Unzip works similar to Untar where support for nested folders is removed
// so that all files are placed in the same target directory
func Unzip(reader io.ReaderAt, size int64, dir string, quiet bool) error {
zipReader, err := zip.NewReader(reader, size)
if err != nil {
return fmt.Errorf("error creating zip reader: %s", err)
}
return unzip(*zipReader, dir, quiet)
}
func unzip(r zip.Reader, dir string, quiet bool) (err error) {
if err != nil {
return err
}
t0 := time.Now()
nFiles := 0
madeDir := map[string]bool{}
defer func() {
td := time.Since(t0)
if err == nil {
if !quiet {
log.Printf("extracted zip into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
}
} else {
log.Printf("error extracting zip into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
}
}()
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
baseFile := filepath.Base(f.Name)
abs := path.Join(dir, baseFile)
if !quiet {
fmt.Printf("Extracting: %s\n", abs)
}
fi := f.FileInfo()
mode := fi.Mode()
switch {
case mode.IsDir():
break
case mode.IsRegular():
f, err := os.OpenFile(abs, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
default:
}
nFiles++
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}
================================================
FILE: vendor/github.com/alexellis/arkade/pkg/env/env.go
================================================
// Copyright (c) arkade author(s) 2022. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package env
import (
"context"
"log"
"os"
"path"
"runtime"
"strings"
execute "github.com/alexellis/go-execute/v2"
)
// GetClientArch returns the architecture and OS of the client machine
// on Windows, that's a direct passthrough from runtime.GOARCH and runtime.GOOS
// On Linux and Darwin, it uses `uname -m` and `uname -s` to get more specific values
func GetClientArch() (arch string, os string) {
if runtime.GOOS == "windows" {
return runtime.GOARCH, runtime.GOOS
}
return getClientArchFromUname()
}
func getClientArchFromUname() (arch string, os string) {
task := execute.ExecTask{
Command: "uname",
Args: []string{"-m"},
StreamStdio: false}
res, err := task.Execute(context.Background())
if err != nil {
log.Println(err)
}
archResult := strings.TrimSpace(res.Stdout)
taskOS := execute.ExecTask{Command: "uname",
Args: []string{"-s"},
StreamStdio: false}
resOS, errOS := taskOS.Execute(context.Background())
if errOS != nil {
log.Println(errOS)
}
osResult := strings.TrimSpace(resOS.Stdout)
return archResult, osResult
}
func LocalBinary(name, subdir string) string {
home := os.Getenv("HOME")
val := path.Join(home, ".arkade/bin/")
if len(subdir) > 0 {
val = path.Join(val, subdir)
}
return path.Join(val, name)
}
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/.DEREK.yml
================================================
redirect: https://raw.githubusercontent.com/openfaas/faas/master/.DEREK.yml
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/.gitignore
================================================
/go-execute
main.go
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/LICENSE
================================================
MIT License
Copyright (c) 2023 Alex Ellis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/Makefile
================================================
all:
go test -v ./...
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/README.md
================================================
## go-execute
A wrapper for Go's command execution packages.
`go get github.com/alexellis/go-execute/v2`
## Docs
See docs at pkg.go.dev: [github.com/alexellis/go-execute](https://pkg.go.dev/github.com/alexellis/go-execute)
## go-execute users
[Used by dozens of projects as identified by GitHub](https://github.com/alexellis/go-execute/network/dependents), notably:
* [alexellis/arkade](https://github.com/alexellis/arkade)
* [openfaas/faas-cli](https://github.com/openfaas/faas-cli)
* [inlets/inletsctl](https://github.com/inlets/inletsctl)
* [inlets/cloud-provision](https://github.com/inlets/cloud-provision)
* [alexellis/k3sup](https://github.com/alexellis/k3sup)
* [openfaas/connector-sdk](https://github.com/openfaas/connector-sdk)
* [openfaas-incubator/ofc-bootstrap](https://github.com/openfaas-incubator/ofc-bootstrap)
Community examples:
* [dokku/lambda-builder](https://github.com/dokku/lambda-builder)
* [027xiguapi/pear-rec](https://github.com/027xiguapi/pear-rec)
* [cnrancher/autok3s](https://github.com/cnrancher/autok3s)
* [ainsleydev/hupi](https://github.com/ainsleydev/hupi)
* [andiwork/andictl](https://github.com/andiwork/andictl)
* [tonit/rekind](https://github.com/tonit/rekind)
* [lucasrod16/ec2-k3s](https://github.com/lucasrod16/ec2-k3s)
* [seaweedfs/seaweed-up](https://github.com/seaweedfs/seaweed-up)
* [jsiebens/inlets-on-fly](https://github.com/jsiebens/inlets-on-fly)
* [jsiebens/hashi-up](https://github.com/jsiebens/hashi-up)
* [edgego/ecm](https://github.com/edgego/ecm)
* [ministryofjustice/cloud-platform-terraform-upgrade](https://github.com/ministryofjustice/cloud-platform-terraform-upgrade)
* [mattcanty/go-ffmpeg-transcode](https://github.com/mattcanty/go-ffmpeg-transcode)
* [Popoola-Opeyemi/meeseeks](https://github.com/Popoola-Opeyemi/meeseeks)
* [aidun/minicloud](https://github.com/aidun/minicloud)
Feel free to add a link to your own projects in a PR.
## Main options
* `DisableStdioBuffer` - Discard Stdio, rather than buffering into memory
* `StreamStdio` - Stream stderr and stdout to the console, useful for debugging and testing
* `Shell` - Use bash as a shell to execute the command, rather than exec a binary directly
* `StdOutWriter` - an additional writer for stdout, useful for mutating or filtering the output
* `StdErrWriter` - an additional writer for stderr, useful for mutating or filtering the output
* `PrintCommand` - print the command to stdout before executing it
## Example of exec without streaming to STDIO
This example captures the values from stdout and stderr without relaying to the console. This means the values can be inspected and used for automation.
```golang
package main
import (
"fmt"
execute "github.com/alexellis/go-execute/v2"
"context"
)
func main() {
cmd := execute.ExecTask{
Command: "docker",
Args: []string{"version"},
StreamStdio: false,
}
res, err := cmd.Execute(context.Background())
if err != nil {
panic(err)
}
if res.ExitCode != 0 {
panic("Non-zero exit code: " + res.Stderr)
}
fmt.Printf("stdout: %s, stderr: %s, exit-code: %d\n", res.Stdout, res.Stderr, res.ExitCode)
}
```
## Example with "shell" and exit-code 0
```golang
package main
import (
"fmt"
execute "github.com/alexellis/go-execute/v2"
"context"
)
func main() {
ls := execute.ExecTask{
Command: "ls",
Args: []string{"-l"},
Shell: true,
}
res, err := ls.Execute(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("stdout: %q, stderr: %q, exit-code: %d\n", res.Stdout, res.Stderr, res.ExitCode)
}
```
## Example with "shell" and exit-code 1
```golang
package main
import (
"fmt"
"context"
execute "github.com/alexellis/go-execute/v2"
)
func main() {
ls := execute.ExecTask{
Command: "exit 1",
Shell: true,
}
res, err := ls.Execute(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("stdout: %q, stderr: %q, exit-code: %d\n", res.Stdout, res.Stderr, res.ExitCode)
}
```
## Contributing
Commits must be signed off with `git commit -s`
License: MIT
================================================
FILE: vendor/github.com/alexellis/go-execute/v2/exec.go
================================================
package execute
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
type ExecTask struct {
// Command is the command to execute. This can be the path to an executable
// or the executable with arguments. The arguments are detected by looking for
// a space.
//
// Any arguments must be given via Args
Command string
// Args are the arguments to pass to the command. These are ignored if the
// Command contains arguments.
Args []string
// Shell run the command in a bash shell.
// Note that the system must have `bash` installed in the PATH or in /bin/bash
Shell bool
// Env is a list of environment variables to add to the current environment,
// these are used to override any existing environment variables.
Env []string
// Cwd is the working directory for the command
Cwd string
// Stdin connect a reader to stdin for the command
// being executed.
Stdin io.Reader
// PrintCommand prints the command before executing
PrintCommand bool
// StreamStdio prints stdout and stderr directly to os.Stdout/err as
// the command runs.
StreamStdio bool
// DisableStdioBuffer prevents any output from being saved in the
// TaskResult, which is useful for when the result is very large, or
// when you want to stream the output to another writer exclusively.
DisableStdioBuffer bool
// StdoutWriter when set will receive a copy of stdout from the command
StdOutWriter io.Writer
// StderrWriter when set will receive a copy of stderr from the command
StdErrWriter io.Writer
}
type ExecResult struct {
Stdout string
Stderr string
ExitCode int
Cancelled bool
}
func (et ExecTask) Execute(ctx context.Context) (ExecResult, error) {
argsSt := ""
if len(et.Args) > 0 {
argsSt = strings.Join(et.Args, " ")
}
if et.PrintCommand {
fmt.Println("exec: ", et.Command, argsSt)
}
// don't try to run if the context is already cancelled
if ctx.Err() != nil {
return ExecResult{
// the exec package returns -1 for cancelled commands
ExitCode: -1,
Cancelled: ctx.Err() == context.Canceled,
}, ctx.Err()
}
var command string
var commandArgs []string
if et.Shell {
// On a NixOS system, /bin/bash doesn't exist at /bin/bash
// the default behavior of exec.Command is to look for the
// executable in PATH.
command = "bash"
// There is a chance that PATH is not populate or propagated, therefore
// when bash cannot be resolved, set it to /bin/bash instead.
if _, err := exec.LookPath(command); err != nil {
command = "/bin/bash"
}
if len(et.Args) == 0 {
// use Split and Join to remove any extra whitespace?
startArgs := strings.Split(et.Command, " ")
script := strings.Join(startArgs, " ")
commandArgs = append([]string{"-c"}, script)
} else {
script := strings.Join(et.Args, " ")
commandArgs = append([]string{"-c"}, fmt.Sprintf("%s %s", et.Command, script))
}
} else {
command = et.Command
commandArgs = et.Args
// AE: This had to be removed to fix: #117 where Windows users
// have spaces in their paths, which are misinterpreted as
// arguments for the command.
// if strings.Contains(et.Command, " ") {
// parts := strings.Split(et.Command, " ")
// command = parts[0]
// commandArgs = parts[1:]
// }
}
cmd := exec.CommandContext(ctx, command, commandArgs...)
cmd.Dir = et.Cwd
if len(et.Env) > 0 {
overrides := map[string]bool{}
for _, env := range et.Env {
key := strings.Split(env, "=")[0]
overrides[key] = true
cmd.Env = append(cmd.Env, env)
}
for _, env := range os.Environ() {
key := strings.Split(env, "=")[0]
if _, ok := overrides[key]; !ok {
cmd.Env = append(cmd.Env, env)
}
}
}
if et.Stdin != nil {
cmd.Stdin = et.Stdin
}
stdoutBuff := bytes.Buffer{}
stderrBuff := bytes.Buffer{}
var stdoutWriters []io.Writer
var stderrWriters []io.Writer
if !et.DisableStdioBuffer {
stdoutWriters = append(stdoutWriters, &stdoutBuff)
stderrWriters = append(stderrWriters, &stderrBuff)
}
if et.StreamStdio {
stdoutWriters = append(stdoutWriters, os.Stdout)
stderrWriters = append(stderrWriters, os.Stderr)
}
if et.StdOutWriter != nil {
stdoutWriters = append(stdoutWriters, et.StdOutWriter)
}
if et.StdErrWriter != nil {
stderrWriters = append(stderrWriters, et.StdErrWriter)
}
cmd.Stdout = io.MultiWriter(stdoutWriters...)
cmd.Stderr = io.MultiWriter(stderrWriters...)
startErr := cmd.Start()
if startErr != nil {
return ExecResult{}, startErr
}
exitCode := 0
execErr := cmd.Wait()
if execErr != nil {
if exitError, ok := execErr.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
}
}
return ExecResult{
Stdout: stdoutBuff.String(),
Stderr: stderrBuff.String(),
ExitCode: exitCode,
Cancelled: ctx.Err() == context.Canceled,
}, ctx.Err()
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/build.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
package estargz
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"os"
"path"
"runtime"
"strings"
"sync"
"sync/atomic"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
"github.com/klauspost/compress/zstd"
digest "github.com/opencontainers/go-digest"
"golang.org/x/sync/errgroup"
)
type GzipHelperFunc func(io.Reader) (io.ReadCloser, error)
type options struct {
chunkSize int
compressionLevel int
prioritizedFiles []string
missedPrioritizedFiles *[]string
compression Compression
ctx context.Context
minChunkSize int
gzipHelperFunc GzipHelperFunc
}
type Option func(o *options) error
// WithChunkSize option specifies the chunk size of eStargz blob to build.
func WithChunkSize(chunkSize int) Option {
return func(o *options) error {
o.chunkSize = chunkSize
return nil
}
}
// WithCompressionLevel option specifies the gzip compression level.
// The default is gzip.BestCompression.
// This option will be ignored if WithCompression option is used.
// See also: https://godoc.org/compress/gzip#pkg-constants
func WithCompressionLevel(level int) Option {
return func(o *options) error {
o.compressionLevel = level
return nil
}
}
// WithPrioritizedFiles option specifies the list of prioritized files.
// These files must be complete paths that are absolute or relative to "/"
// For example, all of "foo/bar", "/foo/bar", "./foo/bar" and "../foo/bar"
// are treated as "/foo/bar".
func WithPrioritizedFiles(files []string) Option {
return func(o *options) error {
o.prioritizedFiles = files
return nil
}
}
// WithAllowPrioritizeNotFound makes Build continue the execution even if some
// of prioritized files specified by WithPrioritizedFiles option aren't found
// in the input tar. Instead, this records all missed file names to the passed
// slice.
func WithAllowPrioritizeNotFound(missedFiles *[]string) Option {
return func(o *options) error {
if missedFiles == nil {
return fmt.Errorf("WithAllowPrioritizeNotFound: slice must be passed")
}
o.missedPrioritizedFiles = missedFiles
return nil
}
}
// WithCompression specifies compression algorithm to be used.
// Default is gzip.
func WithCompression(compression Compression) Option {
return func(o *options) error {
o.compression = compression
return nil
}
}
// WithContext specifies a context that can be used for clean canceleration.
func WithContext(ctx context.Context) Option {
return func(o *options) error {
o.ctx = ctx
return nil
}
}
// WithMinChunkSize option specifies the minimal number of bytes of data
// must be written in one gzip stream.
// By increasing this number, one gzip stream can contain multiple files
// and it hopefully leads to smaller result blob.
// NOTE: This adds a TOC property that old reader doesn't understand.
func WithMinChunkSize(minChunkSize int) Option {
return func(o *options) error {
o.minChunkSize = minChunkSize
return nil
}
}
// WithGzipHelperFunc option specifies a custom function to decompress gzip-compressed layers.
// When a gzip-compressed layer is detected, this function will be used instead of the
// Go standard library gzip decompression for better performance.
// The function should take an io.Reader as input and return an io.ReadCloser.
// If nil, the Go standard library gzip.NewReader will be used.
func WithGzipHelperFunc(gzipHelperFunc GzipHelperFunc) Option {
return func(o *options) error {
o.gzipHelperFunc = gzipHelperFunc
return nil
}
}
// Blob is an eStargz blob.
type Blob struct {
io.ReadCloser
diffID digest.Digester
tocDigest digest.Digest
readCompleted *atomic.Bool
uncompressedSize *atomic.Int64
}
// DiffID returns the digest of uncompressed blob.
// It is only valid to call DiffID after Close.
func (b *Blob) DiffID() digest.Digest {
return b.diffID.Digest()
}
// TOCDigest returns the digest of uncompressed TOC JSON.
func (b *Blob) TOCDigest() digest.Digest {
return b.tocDigest
}
// UncompressedSize returns the size of uncompressed blob.
// UncompressedSize should only be called after the blob has been fully read.
func (b *Blob) UncompressedSize() (int64, error) {
switch {
case b.uncompressedSize == nil || b.readCompleted == nil:
return -1, fmt.Errorf("readCompleted or uncompressedSize is not initialized")
case !b.readCompleted.Load():
return -1, fmt.Errorf("called UncompressedSize before the blob has been fully read")
default:
return b.uncompressedSize.Load(), nil
}
}
// Build builds an eStargz blob which is an extended version of stargz, from a blob (gzip, zstd
// or plain tar) passed through the argument. If there are some prioritized files are listed in
// the option, these files are grouped as "prioritized" and can be used for runtime optimization
// (e.g. prefetch). This function builds a blob in parallel, with dividing that blob into several
// (at least the number of runtime.GOMAXPROCS(0)) sub-blobs.
func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) {
var opts options
opts.compressionLevel = gzip.BestCompression // BestCompression by default
for _, o := range opt {
if err := o(&opts); err != nil {
return nil, err
}
}
if opts.compression == nil {
opts.compression = newGzipCompressionWithLevel(opts.compressionLevel)
}
layerFiles := newTempFiles()
ctx := opts.ctx
if ctx == nil {
ctx = context.Background()
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
// nop
case <-ctx.Done():
layerFiles.CleanupAll()
}
}()
defer func() {
if rErr != nil {
if err := layerFiles.CleanupAll(); err != nil {
rErr = fmt.Errorf("failed to cleanup tmp files: %v: %w", err, rErr)
}
}
if cErr := ctx.Err(); cErr != nil {
rErr = fmt.Errorf("error from context %q: %w", cErr, rErr)
}
}()
tarBlob, err := decompressBlob(tarBlob, layerFiles, opts.gzipHelperFunc)
if err != nil {
return nil, err
}
entries, err := sortEntries(tarBlob, opts.prioritizedFiles, opts.missedPrioritizedFiles)
if err != nil {
return nil, err
}
var tarParts [][]*entry
if opts.minChunkSize > 0 {
// Each entry needs to know the size of the current gzip stream so they
// cannot be processed in parallel.
tarParts = [][]*entry{entries}
} else {
tarParts = divideEntries(entries, runtime.GOMAXPROCS(0))
}
writers := make([]*Writer, len(tarParts))
payloads := make([]*os.File, len(tarParts))
var mu sync.Mutex
var eg errgroup.Group
for i, parts := range tarParts {
i, parts := i, parts
// builds verifiable stargz sub-blobs
eg.Go(func() error {
esgzFile, err := layerFiles.TempFile("", "esgzdata")
if err != nil {
return err
}
sw := NewWriterWithCompressor(esgzFile, opts.compression)
sw.ChunkSize = opts.chunkSize
sw.MinChunkSize = opts.minChunkSize
if sw.needsOpenGzEntries == nil {
sw.needsOpenGzEntries = make(map[string]struct{})
}
for _, f := range []string{PrefetchLandmark, NoPrefetchLandmark} {
sw.needsOpenGzEntries[f] = struct{}{}
}
if err := sw.AppendTar(readerFromEntries(parts...)); err != nil {
return err
}
mu.Lock()
writers[i] = sw
payloads[i] = esgzFile
mu.Unlock()
return nil
})
}
if err := eg.Wait(); err != nil {
rErr = err
return nil, err
}
tocAndFooter, tocDgst, err := closeWithCombine(writers...)
if err != nil {
rErr = err
return nil, err
}
var rs []io.Reader
for _, p := range payloads {
fs, err := fileSectionReader(p)
if err != nil {
return nil, err
}
rs = append(rs, fs)
}
diffID := digest.Canonical.Digester()
pr, pw := io.Pipe()
readCompleted := new(atomic.Bool)
uncompressedSize := new(atomic.Int64)
go func() {
var size int64
var decompressFunc func(io.Reader) (io.ReadCloser, error)
if _, ok := opts.compression.(*gzipCompression); ok && opts.gzipHelperFunc != nil {
decompressFunc = opts.gzipHelperFunc
} else {
decompressFunc = opts.compression.Reader
}
decompressR, err := decompressFunc(io.TeeReader(io.MultiReader(append(rs, tocAndFooter)...), pw))
if err != nil {
pw.CloseWithError(err)
return
}
defer decompressR.Close()
if size, err = io.Copy(diffID.Hash(), decompressR); err != nil {
pw.CloseWithError(err)
return
}
uncompressedSize.Store(size)
readCompleted.Store(true)
pw.Close()
}()
return &Blob{
ReadCloser: readCloser{
Reader: pr,
closeFunc: layerFiles.CleanupAll,
},
tocDigest: tocDgst,
diffID: diffID,
readCompleted: readCompleted,
uncompressedSize: uncompressedSize,
}, nil
}
// closeWithCombine takes unclosed Writers and close them. This also returns the
// toc that combined all Writers into.
// Writers doesn't write TOC and footer to the underlying writers so they can be
// combined into a single eStargz and tocAndFooter returned by this function can
// be appended at the tail of that combined blob.
func closeWithCombine(ws ...*Writer) (tocAndFooterR io.Reader, tocDgst digest.Digest, err error) {
if len(ws) == 0 {
return nil, "", fmt.Errorf("at least one writer must be passed")
}
for _, w := range ws {
if w.closed {
return nil, "", fmt.Errorf("writer must be unclosed")
}
defer func(w *Writer) { w.closed = true }(w)
if err := w.closeGz(); err != nil {
return nil, "", err
}
if err := w.bw.Flush(); err != nil {
return nil, "", err
}
}
var (
mtoc = new(JTOC)
currentOffset int64
)
mtoc.Version = ws[0].toc.Version
for _, w := range ws {
for _, e := range w.toc.Entries {
// Recalculate Offset of non-empty files/chunks
if (e.Type == "reg" && e.Size > 0) || e.Type == "chunk" {
e.Offset += currentOffset
}
mtoc.Entries = append(mtoc.Entries, e)
}
if w.toc.Version > mtoc.Version {
mtoc.Version = w.toc.Version
}
currentOffset += w.cw.n
}
return tocAndFooter(ws[0].compressor, mtoc, currentOffset)
}
func tocAndFooter(compressor Compressor, toc *JTOC, offset int64) (io.Reader, digest.Digest, error) {
buf := new(bytes.Buffer)
tocDigest, err := compressor.WriteTOCAndFooter(buf, offset, toc, nil)
if err != nil {
return nil, "", err
}
return buf, tocDigest, nil
}
// divideEntries divides passed entries to the parts at least the number specified by the
// argument.
func divideEntries(entries []*entry, minPartsNum int) (set [][]*entry) {
var estimatedSize int64
for _, e := range entries {
estimatedSize += e.header.Size
}
unitSize := estimatedSize / int64(minPartsNum)
var (
nextEnd = unitSize
offset int64
)
set = append(set, []*entry{})
for _, e := range entries {
set[len(set)-1] = append(set[len(set)-1], e)
offset += e.header.Size
if offset > nextEnd {
set = append(set, []*entry{})
nextEnd += unitSize
}
}
return
}
var errNotFound = errors.New("not found")
// sortEntries reads the specified tar blob and returns a list of tar entries.
// If some of prioritized files are specified, the list starts from these
// files with keeping the order specified by the argument.
func sortEntries(in io.ReaderAt, prioritized []string, missedPrioritized *[]string) ([]*entry, error) {
// Import tar file.
intar, err := importTar(in)
if err != nil {
return nil, fmt.Errorf("failed to sort: %w", err)
}
// Sort the tar file respecting to the prioritized files list.
sorted := &tarFile{}
picked := make(map[string]struct{})
for _, l := range prioritized {
if err := moveRec(l, intar, sorted, picked); err != nil {
if errors.Is(err, errNotFound) && missedPrioritized != nil {
*missedPrioritized = append(*missedPrioritized, l)
continue // allow not found
}
return nil, fmt.Errorf("failed to sort tar entries: %w", err)
}
}
if len(prioritized) == 0 {
sorted.add(&entry{
header: &tar.Header{
Name: NoPrefetchLandmark,
Typeflag: tar.TypeReg,
Size: int64(len([]byte{landmarkContents})),
},
payload: bytes.NewReader([]byte{landmarkContents}),
})
} else {
sorted.add(&entry{
header: &tar.Header{
Name: PrefetchLandmark,
Typeflag: tar.TypeReg,
Size: int64(len([]byte{landmarkContents})),
},
payload: bytes.NewReader([]byte{landmarkContents}),
})
}
// Dump prioritized entries followed by the rest entries while skipping picked ones.
return append(sorted.dump(nil), intar.dump(picked)...), nil
}
// readerFromEntries returns a reader of tar archive that contains entries passed
// through the arguments.
func readerFromEntries(entries ...*entry) io.Reader {
pr, pw := io.Pipe()
go func() {
tw := tar.NewWriter(pw)
defer tw.Close()
for _, entry := range entries {
if err := tw.WriteHeader(entry.header); err != nil {
pw.CloseWithError(fmt.Errorf("failed to write tar header: %v", err))
return
}
if _, err := io.Copy(tw, entry.payload); err != nil {
pw.CloseWithError(fmt.Errorf("failed to write tar payload: %v", err))
return
}
}
pw.Close()
}()
return pr
}
func importTar(in io.ReaderAt) (*tarFile, error) {
tf := &tarFile{}
pw, err := newCountReadSeeker(in)
if err != nil {
return nil, fmt.Errorf("failed to make position watcher: %w", err)
}
tr := tar.NewReader(pw)
// Walk through all nodes.
for {
// Fetch and parse next header.
h, err := tr.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("failed to parse tar file, %w", err)
}
switch cleanEntryName(h.Name) {
case PrefetchLandmark, NoPrefetchLandmark:
// Ignore existing landmark
continue
}
// Add entry. If it already exists, replace it.
if _, ok := tf.get(h.Name); ok {
tf.remove(h.Name)
}
tf.add(&entry{
header: h,
payload: io.NewSectionReader(in, pw.currentPos(), h.Size),
})
}
return tf, nil
}
func moveRec(name string, in *tarFile, out *tarFile, picked map[string]struct{}) error {
name = cleanEntryName(name)
if name == "" { // root directory. stop recursion.
if e, ok := in.get(name); ok {
// entry of the root directory exists. we should move it as well.
// this case will occur if tar entries are prefixed with "./", "/", etc.
if _, done := picked[name]; !done {
out.add(e)
picked[name] = struct{}{}
}
}
return nil
}
_, okIn := in.get(name)
_, okOut := out.get(name)
_, okPicked := picked[name]
if !okIn && !okOut && !okPicked {
return fmt.Errorf("file: %q: %w", name, errNotFound)
}
parent, _ := path.Split(strings.TrimSuffix(name, "/"))
if err := moveRec(parent, in, out, picked); err != nil {
return err
}
if e, ok := in.get(name); ok && e.header.Typeflag == tar.TypeLink {
if err := moveRec(e.header.Linkname, in, out, picked); err != nil {
return err
}
}
if _, done := picked[name]; done {
return nil
}
if e, ok := in.get(name); ok {
out.add(e)
picked[name] = struct{}{}
}
return nil
}
type entry struct {
header *tar.Header
payload io.ReadSeeker
}
type tarFile struct {
index map[string]*entry
stream []*entry
}
func (f *tarFile) add(e *entry) {
if f.index == nil {
f.index = make(map[string]*entry)
}
f.index[cleanEntryName(e.header.Name)] = e
f.stream = append(f.stream, e)
}
func (f *tarFile) remove(name string) {
name = cleanEntryName(name)
if f.index != nil {
delete(f.index, name)
}
var filtered []*entry
for _, e := range f.stream {
if cleanEntryName(e.header.Name) == name {
continue
}
filtered = append(filtered, e)
}
f.stream = filtered
}
func (f *tarFile) get(name string) (e *entry, ok bool) {
if f.index == nil {
return nil, false
}
e, ok = f.index[cleanEntryName(name)]
return
}
func (f *tarFile) dump(skip map[string]struct{}) []*entry {
if len(skip) == 0 {
return f.stream
}
var out []*entry
for _, e := range f.stream {
if _, ok := skip[cleanEntryName(e.header.Name)]; ok {
continue
}
out = append(out, e)
}
return out
}
type readCloser struct {
io.Reader
closeFunc func() error
}
func (rc readCloser) Close() error {
return rc.closeFunc()
}
func fileSectionReader(file *os.File) (*io.SectionReader, error) {
info, err := file.Stat()
if err != nil {
return nil, err
}
return io.NewSectionReader(file, 0, info.Size()), nil
}
func newTempFiles() *tempFiles {
return &tempFiles{}
}
type tempFiles struct {
files []*os.File
filesMu sync.Mutex
cleanupOnce sync.Once
}
func (tf *tempFiles) TempFile(dir, pattern string) (*os.File, error) {
f, err := os.CreateTemp(dir, pattern)
if err != nil {
return nil, err
}
tf.filesMu.Lock()
tf.files = append(tf.files, f)
tf.filesMu.Unlock()
return f, nil
}
func (tf *tempFiles) CleanupAll() (err error) {
tf.cleanupOnce.Do(func() {
err = tf.cleanupAll()
})
return
}
func (tf *tempFiles) cleanupAll() error {
tf.filesMu.Lock()
defer tf.filesMu.Unlock()
var allErr []error
for _, f := range tf.files {
if err := f.Close(); err != nil {
allErr = append(allErr, err)
}
if err := os.Remove(f.Name()); err != nil {
allErr = append(allErr, err)
}
}
tf.files = nil
return errorutil.Aggregate(allErr)
}
func newCountReadSeeker(r io.ReaderAt) (*countReadSeeker, error) {
pos := int64(0)
return &countReadSeeker{r: r, cPos: &pos}, nil
}
type countReadSeeker struct {
r io.ReaderAt
cPos *int64
mu sync.Mutex
}
func (cr *countReadSeeker) Read(p []byte) (int, error) {
cr.mu.Lock()
defer cr.mu.Unlock()
n, err := cr.r.ReadAt(p, *cr.cPos)
if err == nil {
*cr.cPos += int64(n)
}
return n, err
}
func (cr *countReadSeeker) Seek(offset int64, whence int) (int64, error) {
cr.mu.Lock()
defer cr.mu.Unlock()
switch whence {
default:
return 0, fmt.Errorf("unknown whence: %v", whence)
case io.SeekStart:
case io.SeekCurrent:
offset += *cr.cPos
case io.SeekEnd:
return 0, fmt.Errorf("unsupported whence: %v", whence)
}
if offset < 0 {
return 0, fmt.Errorf("invalid offset")
}
*cr.cPos = offset
return offset, nil
}
func (cr *countReadSeeker) currentPos() int64 {
cr.mu.Lock()
defer cr.mu.Unlock()
return *cr.cPos
}
func decompressBlob(org *io.SectionReader, tmp *tempFiles, gzipHelperFunc GzipHelperFunc) (*io.SectionReader, error) {
if org.Size() < 4 {
return org, nil
}
src := make([]byte, 4)
if _, err := org.Read(src); err != nil && err != io.EOF {
return nil, err
}
var dR io.Reader
if bytes.Equal([]byte{0x1F, 0x8B, 0x08}, src[:3]) {
// gzip
var dgR io.ReadCloser
var err error
if gzipHelperFunc != nil {
dgR, err = gzipHelperFunc(io.NewSectionReader(org, 0, org.Size()))
} else {
dgR, err = gzip.NewReader(io.NewSectionReader(org, 0, org.Size()))
}
if err != nil {
return nil, err
}
defer dgR.Close()
dR = io.Reader(dgR)
} else if bytes.Equal([]byte{0x28, 0xb5, 0x2f, 0xfd}, src[:4]) {
// zstd
dzR, err := zstd.NewReader(io.NewSectionReader(org, 0, org.Size()))
if err != nil {
return nil, err
}
defer dzR.Close()
dR = io.Reader(dzR)
} else {
// uncompressed
return io.NewSectionReader(org, 0, org.Size()), nil
}
b, err := tmp.TempFile("", "uncompresseddata")
if err != nil {
return nil, err
}
if _, err := io.Copy(b, dR); err != nil {
return nil, err
}
return fileSectionReader(b)
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/errorutil/errors.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package errorutil
import (
"errors"
"fmt"
"strings"
)
// Aggregate combines a list of errors into a single new error.
func Aggregate(errs []error) error {
switch len(errs) {
case 0:
return nil
case 1:
return errs[0]
default:
points := make([]string, len(errs)+1)
points[0] = fmt.Sprintf("%d error(s) occurred:", len(errs))
for i, err := range errs {
points[i+1] = fmt.Sprintf("* %s", err)
}
return errors.New(strings.Join(points, "\n\t"))
}
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/estargz.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
package estargz
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"hash"
"io"
"os"
"path"
"sort"
"strings"
"sync"
"time"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
digest "github.com/opencontainers/go-digest"
"github.com/vbatts/tar-split/archive/tar"
)
// A Reader permits random access reads from a stargz file.
type Reader struct {
sr *io.SectionReader
toc *JTOC
tocDigest digest.Digest
// m stores all non-chunk entries, keyed by name.
m map[string]*TOCEntry
// chunks stores all TOCEntry values for regular files that
// are split up. For a file with a single chunk, it's only
// stored in m.
chunks map[string][]*TOCEntry
decompressor Decompressor
}
type openOpts struct {
tocOffset int64
decompressors []Decompressor
telemetry *Telemetry
}
// OpenOption is an option used during opening the layer
type OpenOption func(o *openOpts) error
// WithTOCOffset option specifies the offset of TOC
func WithTOCOffset(tocOffset int64) OpenOption {
return func(o *openOpts) error {
o.tocOffset = tocOffset
return nil
}
}
// WithDecompressors option specifies decompressors to use.
// Default is gzip-based decompressor.
func WithDecompressors(decompressors ...Decompressor) OpenOption {
return func(o *openOpts) error {
o.decompressors = decompressors
return nil
}
}
// WithTelemetry option specifies the telemetry hooks
func WithTelemetry(telemetry *Telemetry) OpenOption {
return func(o *openOpts) error {
o.telemetry = telemetry
return nil
}
}
// MeasureLatencyHook is a func which takes start time and records the diff
type MeasureLatencyHook func(time.Time)
// Telemetry is a struct which defines telemetry hooks. By implementing these hooks you should be able to record
// the latency metrics of the respective steps of estargz open operation. To be used with estargz.OpenWithTelemetry(...)
type Telemetry struct {
GetFooterLatency MeasureLatencyHook // measure time to get stargz footer (in milliseconds)
GetTocLatency MeasureLatencyHook // measure time to GET TOC JSON (in milliseconds)
DeserializeTocLatency MeasureLatencyHook // measure time to deserialize TOC JSON (in milliseconds)
}
// Open opens a stargz file for reading.
// The behavior is configurable using options.
//
// Note that each entry name is normalized as the path that is relative to root.
func Open(sr *io.SectionReader, opt ...OpenOption) (*Reader, error) {
var opts openOpts
for _, o := range opt {
if err := o(&opts); err != nil {
return nil, err
}
}
gzipCompressors := []Decompressor{new(GzipDecompressor), new(LegacyGzipDecompressor)}
decompressors := append(gzipCompressors, opts.decompressors...)
// Determine the size to fetch. Try to fetch as many bytes as possible.
fetchSize := maxFooterSize(sr.Size(), decompressors...)
if maybeTocOffset := opts.tocOffset; maybeTocOffset > fetchSize {
if maybeTocOffset > sr.Size() {
return nil, fmt.Errorf("blob size %d is smaller than the toc offset", sr.Size())
}
fetchSize = sr.Size() - maybeTocOffset
}
start := time.Now() // before getting layer footer
footer := make([]byte, fetchSize)
if _, err := sr.ReadAt(footer, sr.Size()-fetchSize); err != nil {
return nil, fmt.Errorf("error reading footer: %v", err)
}
if opts.telemetry != nil && opts.telemetry.GetFooterLatency != nil {
opts.telemetry.GetFooterLatency(start)
}
var allErr []error
var found bool
var r *Reader
for _, d := range decompressors {
fSize := d.FooterSize()
fOffset := positive(int64(len(footer)) - fSize)
maybeTocBytes := footer[:fOffset]
_, tocOffset, tocSize, err := d.ParseFooter(footer[fOffset:])
if err != nil {
allErr = append(allErr, err)
continue
}
if tocOffset >= 0 && tocSize <= 0 {
tocSize = sr.Size() - tocOffset - fSize
}
if tocOffset >= 0 && tocSize < int64(len(maybeTocBytes)) {
maybeTocBytes = maybeTocBytes[:tocSize]
}
r, err = parseTOC(d, sr, tocOffset, tocSize, maybeTocBytes, opts)
if err == nil {
found = true
break
}
allErr = append(allErr, err)
}
if !found {
return nil, errorutil.Aggregate(allErr)
}
if err := r.initFields(); err != nil {
return nil, fmt.Errorf("failed to initialize fields of entries: %v", err)
}
return r, nil
}
// OpenFooter extracts and parses footer from the given blob.
// only supports gzip-based eStargz.
func OpenFooter(sr *io.SectionReader) (tocOffset int64, footerSize int64, rErr error) {
if sr.Size() < FooterSize && sr.Size() < legacyFooterSize {
return 0, 0, fmt.Errorf("blob size %d is smaller than the footer size", sr.Size())
}
var footer [FooterSize]byte
if _, err := sr.ReadAt(footer[:], sr.Size()-FooterSize); err != nil {
return 0, 0, fmt.Errorf("error reading footer: %v", err)
}
var allErr []error
for _, d := range []Decompressor{new(GzipDecompressor), new(LegacyGzipDecompressor)} {
fSize := d.FooterSize()
fOffset := positive(int64(len(footer)) - fSize)
_, tocOffset, _, err := d.ParseFooter(footer[fOffset:])
if err == nil {
return tocOffset, fSize, err
}
allErr = append(allErr, err)
}
return 0, 0, errorutil.Aggregate(allErr)
}
// initFields populates the Reader from r.toc after decoding it from
// JSON.
//
// Unexported fields are populated and TOCEntry fields that were
// implicit in the JSON are populated.
func (r *Reader) initFields() error {
r.m = make(map[string]*TOCEntry, len(r.toc.Entries))
r.chunks = make(map[string][]*TOCEntry)
var lastPath string
uname := map[int]string{}
gname := map[int]string{}
var lastRegEnt *TOCEntry
var chunkTopIndex int
for i, ent := range r.toc.Entries {
ent.Name = cleanEntryName(ent.Name)
switch ent.Type {
case "reg", "chunk":
if ent.Offset != r.toc.Entries[chunkTopIndex].Offset {
chunkTopIndex = i
}
ent.chunkTopIndex = chunkTopIndex
}
if ent.Type == "reg" {
lastRegEnt = ent
}
if ent.Type == "chunk" {
ent.Name = lastPath
r.chunks[ent.Name] = append(r.chunks[ent.Name], ent)
if ent.ChunkSize == 0 && lastRegEnt != nil {
ent.ChunkSize = lastRegEnt.Size - ent.ChunkOffset
}
} else {
lastPath = ent.Name
if ent.Uname != "" {
uname[ent.UID] = ent.Uname
} else {
ent.Uname = uname[ent.UID]
}
if ent.Gname != "" {
gname[ent.GID] = ent.Gname
} else {
ent.Gname = gname[ent.GID]
}
ent.modTime, _ = time.Parse(time.RFC3339, ent.ModTime3339)
if ent.Type == "dir" {
ent.NumLink++ // Parent dir links to this directory
}
r.m[ent.Name] = ent
}
if ent.Type == "reg" && ent.ChunkSize > 0 && ent.ChunkSize < ent.Size {
r.chunks[ent.Name] = make([]*TOCEntry, 0, ent.Size/ent.ChunkSize+1)
r.chunks[ent.Name] = append(r.chunks[ent.Name], ent)
}
if ent.ChunkSize == 0 && ent.Size != 0 {
ent.ChunkSize = ent.Size
}
}
// Populate children, add implicit directories:
for _, ent := range r.toc.Entries {
if ent.Type == "chunk" {
continue
}
// add "foo/":
// add "foo" child to "" (creating "" if necessary)
//
// add "foo/bar/":
// add "bar" child to "foo" (creating "foo" if necessary)
//
// add "foo/bar.txt":
// add "bar.txt" child to "foo" (creating "foo" if necessary)
//
// add "a/b/c/d/e/f.txt":
// create "a/b/c/d/e" node
// add "f.txt" child to "e"
name := ent.Name
pdirName := parentDir(name)
if name == pdirName {
// This entry and its parent are the same.
// Ignore this for avoiding infinite loop of the reference.
// The example case where this can occur is when tar contains the root
// directory itself (e.g. "./", "/").
continue
}
pdir := r.getOrCreateDir(pdirName)
ent.NumLink++ // at least one name(ent.Name) references this entry.
if ent.Type == "hardlink" {
org, err := r.getSource(ent)
if err != nil {
return err
}
org.NumLink++ // original entry is referenced by this ent.Name.
ent = org
}
pdir.addChild(path.Base(name), ent)
}
lastOffset := r.sr.Size()
for i := len(r.toc.Entries) - 1; i >= 0; i-- {
e := r.toc.Entries[i]
if e.isDataType() {
e.nextOffset = lastOffset
}
if e.Offset != 0 && e.InnerOffset == 0 {
lastOffset = e.Offset
}
}
if len(r.m) == 0 {
r.m[""] = &TOCEntry{
Name: "",
Type: "dir",
Mode: 0755,
NumLink: 1,
}
}
return nil
}
func (r *Reader) getSource(ent *TOCEntry) (_ *TOCEntry, err error) {
if ent.Type == "hardlink" {
org, ok := r.m[cleanEntryName(ent.LinkName)]
if !ok {
return nil, fmt.Errorf("%q is a hardlink but the linkname %q isn't found", ent.Name, ent.LinkName)
}
ent, err = r.getSource(org)
if err != nil {
return nil, err
}
}
return ent, nil
}
func parentDir(p string) string {
dir, _ := path.Split(p)
return strings.TrimSuffix(dir, "/")
}
func (r *Reader) getOrCreateDir(d string) *TOCEntry {
e, ok := r.m[d]
if !ok {
e = &TOCEntry{
Name: d,
Type: "dir",
Mode: 0755,
NumLink: 2, // The directory itself(.) and the parent link to this directory.
}
r.m[d] = e
if d != "" {
pdir := r.getOrCreateDir(parentDir(d))
pdir.addChild(path.Base(d), e)
}
}
return e
}
func (r *Reader) TOCDigest() digest.Digest {
return r.tocDigest
}
// VerifyTOC checks that the TOC JSON in the passed blob matches the
// passed digests and that the TOC JSON contains digests for all chunks
// contained in the blob. If the verification succceeds, this function
// returns TOCEntryVerifier which holds all chunk digests in the stargz blob.
func (r *Reader) VerifyTOC(tocDigest digest.Digest) (TOCEntryVerifier, error) {
// Verify the digest of TOC JSON
if r.tocDigest != tocDigest {
return nil, fmt.Errorf("invalid TOC JSON %q; want %q", r.tocDigest, tocDigest)
}
return r.Verifiers()
}
// Verifiers returns TOCEntryVerifier of this chunk. Use VerifyTOC instead in most cases
// because this doesn't verify TOC.
func (r *Reader) Verifiers() (TOCEntryVerifier, error) {
chunkDigestMap := make(map[int64]digest.Digest) // map from chunk offset to the chunk digest
regDigestMap := make(map[int64]digest.Digest) // map from chunk offset to the reg file digest
var chunkDigestMapIncomplete bool
var regDigestMapIncomplete bool
var containsChunk bool
for _, e := range r.toc.Entries {
if e.Type != "reg" && e.Type != "chunk" {
continue
}
// offset must be unique in stargz blob
_, dOK := chunkDigestMap[e.Offset]
_, rOK := regDigestMap[e.Offset]
if dOK || rOK {
return nil, fmt.Errorf("offset %d found twice", e.Offset)
}
if e.Type == "reg" {
if e.Size == 0 {
continue // ignores empty file
}
// record the digest of regular file payload
if e.Digest != "" {
d, err := digest.Parse(e.Digest)
if err != nil {
return nil, fmt.Errorf("failed to parse regular file digest %q: %w", e.Digest, err)
}
regDigestMap[e.Offset] = d
} else {
regDigestMapIncomplete = true
}
} else {
containsChunk = true // this layer contains "chunk" entries.
}
// "reg" also can contain ChunkDigest (e.g. when "reg" is the first entry of
// chunked file)
if e.ChunkDigest != "" {
d, err := digest.Parse(e.ChunkDigest)
if err != nil {
return nil, fmt.Errorf("failed to parse chunk digest %q: %w", e.ChunkDigest, err)
}
chunkDigestMap[e.Offset] = d
} else {
chunkDigestMapIncomplete = true
}
}
if chunkDigestMapIncomplete {
// Though some chunk digests are not found, if this layer doesn't contain
// "chunk"s and all digest of "reg" files are recorded, we can use them instead.
if !containsChunk && !regDigestMapIncomplete {
return &verifier{digestMap: regDigestMap}, nil
}
return nil, fmt.Errorf("some ChunkDigest not found in TOC JSON")
}
return &verifier{digestMap: chunkDigestMap}, nil
}
// verifier is an implementation of TOCEntryVerifier which holds verifiers keyed by
// offset of the chunk.
type verifier struct {
digestMap map[int64]digest.Digest
digestMapMu sync.Mutex
}
// Verifier returns a content verifier specified by TOCEntry.
func (v *verifier) Verifier(ce *TOCEntry) (digest.Verifier, error) {
v.digestMapMu.Lock()
defer v.digestMapMu.Unlock()
d, ok := v.digestMap[ce.Offset]
if !ok {
return nil, fmt.Errorf("verifier for offset=%d,size=%d hasn't been registered",
ce.Offset, ce.ChunkSize)
}
return d.Verifier(), nil
}
// ChunkEntryForOffset returns the TOCEntry containing the byte of the
// named file at the given offset within the file.
// Name must be absolute path or one that is relative to root.
func (r *Reader) ChunkEntryForOffset(name string, offset int64) (e *TOCEntry, ok bool) {
name = cleanEntryName(name)
e, ok = r.Lookup(name)
if !ok || !e.isDataType() {
return nil, false
}
ents := r.chunks[name]
if len(ents) < 2 {
if offset >= e.ChunkSize {
return nil, false
}
return e, true
}
i := sort.Search(len(ents), func(i int) bool {
e := ents[i]
return e.ChunkOffset >= offset || (offset > e.ChunkOffset && offset < e.ChunkOffset+e.ChunkSize)
})
if i == len(ents) {
return nil, false
}
return ents[i], true
}
// Lookup returns the Table of Contents entry for the given path.
//
// To get the root directory, use the empty string.
// Path must be absolute path or one that is relative to root.
func (r *Reader) Lookup(path string) (e *TOCEntry, ok bool) {
path = cleanEntryName(path)
if r == nil {
return
}
e, ok = r.m[path]
if ok && e.Type == "hardlink" {
var err error
e, err = r.getSource(e)
if err != nil {
return nil, false
}
}
return
}
// OpenFile returns the reader of the specified file payload.
//
// Name must be absolute path or one that is relative to root.
func (r *Reader) OpenFile(name string) (*io.SectionReader, error) {
fr, err := r.newFileReader(name)
if err != nil {
return nil, err
}
return io.NewSectionReader(fr, 0, fr.size), nil
}
func (r *Reader) newFileReader(name string) (*fileReader, error) {
name = cleanEntryName(name)
ent, ok := r.Lookup(name)
if !ok {
// TODO: come up with some error plan. This is lazy:
return nil, &os.PathError{
Path: name,
Op: "OpenFile",
Err: os.ErrNotExist,
}
}
if ent.Type != "reg" {
return nil, &os.PathError{
Path: name,
Op: "OpenFile",
Err: errors.New("not a regular file"),
}
}
return &fileReader{
r: r,
size: ent.Size,
ents: r.getChunks(ent),
}, nil
}
func (r *Reader) OpenFileWithPreReader(name string, preRead func(*TOCEntry, io.Reader) error) (*io.SectionReader, error) {
fr, err := r.newFileReader(name)
if err != nil {
return nil, err
}
fr.preRead = preRead
return io.NewSectionReader(fr, 0, fr.size), nil
}
func (r *Reader) getChunks(ent *TOCEntry) []*TOCEntry {
if ents, ok := r.chunks[ent.Name]; ok {
return ents
}
return []*TOCEntry{ent}
}
type fileReader struct {
r *Reader
size int64
ents []*TOCEntry // 1 or more reg/chunk entries
preRead func(*TOCEntry, io.Reader) error
}
func (fr *fileReader) ReadAt(p []byte, off int64) (n int, err error) {
if off >= fr.size {
return 0, io.EOF
}
if off < 0 {
return 0, errors.New("invalid offset")
}
var i int
if len(fr.ents) > 1 {
i = sort.Search(len(fr.ents), func(i int) bool {
return fr.ents[i].ChunkOffset >= off
})
if i == len(fr.ents) {
i = len(fr.ents) - 1
}
}
ent := fr.ents[i]
if ent.ChunkOffset > off {
if i == 0 {
return 0, errors.New("internal error; first chunk offset is non-zero")
}
ent = fr.ents[i-1]
}
// If ent is a chunk of a large file, adjust the ReadAt
// offset by the chunk's offset.
off -= ent.ChunkOffset
finalEnt := fr.ents[len(fr.ents)-1]
compressedOff := ent.Offset
// compressedBytesRemain is the number of compressed bytes in this
// file remaining, over 1+ chunks.
compressedBytesRemain := finalEnt.NextOffset() - compressedOff
sr := io.NewSectionReader(fr.r.sr, compressedOff, compressedBytesRemain)
const maxRead = 2 << 20
var bufSize = maxRead
if compressedBytesRemain < maxRead {
bufSize = int(compressedBytesRemain)
}
br := bufio.NewReaderSize(sr, bufSize)
if _, err := br.Peek(bufSize); err != nil {
return 0, fmt.Errorf("fileReader.ReadAt.peek: %v", err)
}
dr, err := fr.r.decompressor.Reader(br)
if err != nil {
return 0, fmt.Errorf("fileReader.ReadAt.decompressor.Reader: %v", err)
}
defer dr.Close()
if fr.preRead == nil {
if n, err := io.CopyN(io.Discard, dr, ent.InnerOffset+off); n != ent.InnerOffset+off || err != nil {
return 0, fmt.Errorf("discard of %d bytes != %v, %v", ent.InnerOffset+off, n, err)
}
return io.ReadFull(dr, p)
}
var retN int
var retErr error
var found bool
var nr int64
for _, e := range fr.r.toc.Entries[ent.chunkTopIndex:] {
if !e.isDataType() {
continue
}
if e.Offset != fr.r.toc.Entries[ent.chunkTopIndex].Offset {
break
}
if in, err := io.CopyN(io.Discard, dr, e.InnerOffset-nr); err != nil || in != e.InnerOffset-nr {
return 0, fmt.Errorf("discard of remaining %d bytes != %v, %v", e.InnerOffset-nr, in, err)
}
nr = e.InnerOffset
if e == ent {
found = true
if n, err := io.CopyN(io.Discard, dr, off); n != off || err != nil {
return 0, fmt.Errorf("discard of offset %d bytes != %v, %v", off, n, err)
}
retN, retErr = io.ReadFull(dr, p)
nr += off + int64(retN)
continue
}
cr := &countReader{r: io.LimitReader(dr, e.ChunkSize)}
if err := fr.preRead(e, cr); err != nil {
return 0, fmt.Errorf("failed to pre read: %w", err)
}
nr += cr.n
}
if !found {
return 0, fmt.Errorf("fileReader.ReadAt: target entry not found")
}
return retN, retErr
}
// A Writer writes stargz files.
//
// Use NewWriter to create a new Writer.
type Writer struct {
bw *bufio.Writer
cw *countWriter
toc *JTOC
diffHash hash.Hash // SHA-256 of uncompressed tar
closed bool
gz io.WriteCloser
lastUsername map[int]string
lastGroupname map[int]string
compressor Compressor
uncompressedCounter *countWriteFlusher
// ChunkSize optionally controls the maximum number of bytes
// of data of a regular file that can be written in one gzip
// stream before a new gzip stream is started.
// Zero means to use a default, currently 4 MiB.
ChunkSize int
// MinChunkSize optionally controls the minimum number of bytes
// of data must be written in one gzip stream before a new gzip
// NOTE: This adds a TOC property that stargz snapshotter < v0.13.0 doesn't understand.
MinChunkSize int
needsOpenGzEntries map[string]struct{}
}
// currentCompressionWriter writes to the current w.gz field, which can
// change throughout writing a tar entry.
//
// Additionally, it updates w's SHA-256 of the uncompressed bytes
// of the tar file.
type currentCompressionWriter struct{ w *Writer }
func (ccw currentCompressionWriter) Write(p []byte) (int, error) {
ccw.w.diffHash.Write(p)
if ccw.w.gz == nil {
if err := ccw.w.condOpenGz(); err != nil {
return 0, err
}
}
return ccw.w.gz.Write(p)
}
func (w *Writer) chunkSize() int {
if w.ChunkSize <= 0 {
return 4 << 20
}
return w.ChunkSize
}
// Unpack decompresses the given estargz blob and returns a ReadCloser of the tar blob.
// TOC JSON and footer are removed.
func Unpack(sr *io.SectionReader, c Decompressor) (io.ReadCloser, error) {
footerSize := c.FooterSize()
if sr.Size() < footerSize {
return nil, fmt.Errorf("blob is too small; %d < %d", sr.Size(), footerSize)
}
footerOffset := sr.Size() - footerSize
footer := make([]byte, footerSize)
if _, err := sr.ReadAt(footer, footerOffset); err != nil {
return nil, err
}
blobPayloadSize, _, _, err := c.ParseFooter(footer)
if err != nil {
return nil, fmt.Errorf("failed to parse footer: %w", err)
}
if blobPayloadSize < 0 {
blobPayloadSize = sr.Size()
}
return c.Reader(io.LimitReader(sr, blobPayloadSize))
}
// NewWriter returns a new stargz writer (gzip-based) writing to w.
//
// The writer must be closed to write its trailing table of contents.
func NewWriter(w io.Writer) *Writer {
return NewWriterLevel(w, gzip.BestCompression)
}
// NewWriterLevel returns a new stargz writer (gzip-based) writing to w.
// The compression level is configurable.
//
// The writer must be closed to write its trailing table of contents.
func NewWriterLevel(w io.Writer, compressionLevel int) *Writer {
return NewWriterWithCompressor(w, NewGzipCompressorWithLevel(compressionLevel))
}
// NewWriterWithCompressor returns a new stargz writer writing to w.
// The compression method is configurable.
//
// The writer must be closed to write its trailing table of contents.
func NewWriterWithCompressor(w io.Writer, c Compressor) *Writer {
bw := bufio.NewWriter(w)
cw := &countWriter{w: bw}
return &Writer{
bw: bw,
cw: cw,
toc: &JTOC{Version: 1},
diffHash: sha256.New(),
compressor: c,
uncompressedCounter: &countWriteFlusher{},
}
}
// Close writes the stargz's table of contents and flushes all the
// buffers, returning any error.
func (w *Writer) Close() (digest.Digest, error) {
if w.closed {
return "", nil
}
defer func() { w.closed = true }()
if err := w.closeGz(); err != nil {
return "", err
}
// Write the TOC index and footer.
tocDigest, err := w.compressor.WriteTOCAndFooter(w.cw, w.cw.n, w.toc, w.diffHash)
if err != nil {
return "", err
}
if err := w.bw.Flush(); err != nil {
return "", err
}
return tocDigest, nil
}
func (w *Writer) closeGz() error {
if w.closed {
return errors.New("write on closed Writer")
}
if w.gz != nil {
if err := w.gz.Close(); err != nil {
return err
}
w.gz = nil
}
return nil
}
func (w *Writer) flushGz() error {
if w.closed {
return errors.New("flush on closed Writer")
}
if w.gz != nil {
if f, ok := w.gz.(interface {
Flush() error
}); ok {
return f.Flush()
}
}
return nil
}
// nameIfChanged returns name, unless it was the already the value of (*mp)[id],
// in which case it returns the empty string.
func (w *Writer) nameIfChanged(mp *map[int]string, id int, name string) string {
if name == "" {
return ""
}
if *mp == nil {
*mp = make(map[int]string)
}
if (*mp)[id] == name {
return ""
}
(*mp)[id] = name
return name
}
func (w *Writer) condOpenGz() (err error) {
if w.gz == nil {
w.gz, err = w.compressor.Writer(w.cw)
if w.gz != nil {
w.gz = w.uncompressedCounter.register(w.gz)
}
}
return
}
// AppendTar reads the tar or tar.gz file from r and appends
// each of its contents to w.
//
// The input r can optionally be gzip compressed but the output will
// always be compressed by the specified compressor.
func (w *Writer) AppendTar(r io.Reader) error {
return w.appendTar(r, false)
}
// AppendTarLossLess reads the tar or tar.gz file from r and appends
// each of its contents to w.
//
// The input r can optionally be gzip compressed but the output will
// always be compressed by the specified compressor.
//
// The difference of this func with AppendTar is that this writes
// the input tar stream into w without any modification (e.g. to header bytes).
//
// Note that if the input tar stream already contains TOC JSON, this returns
// error because w cannot overwrite the TOC JSON to the one generated by w without
// lossy modification. To avoid this error, if the input stream is known to be stargz/estargz,
// you shoud decompress it and remove TOC JSON in advance.
func (w *Writer) AppendTarLossLess(r io.Reader) error {
return w.appendTar(r, true)
}
func (w *Writer) appendTar(r io.Reader, lossless bool) error {
var src io.Reader
br := bufio.NewReader(r)
if isGzip(br) {
zr, _ := gzip.NewReader(br)
src = zr
} else {
src = io.Reader(br)
}
dst := currentCompressionWriter{w}
var tw *tar.Writer
if !lossless {
tw = tar.NewWriter(dst) // use tar writer only when this isn't lossless mode.
}
tr := tar.NewReader(src)
if lossless {
tr.RawAccounting = true
}
prevOffset := w.cw.n
var prevOffsetUncompressed int64
for {
h, err := tr.Next()
if err == io.EOF {
if lossless {
if remain := tr.RawBytes(); len(remain) > 0 {
// Collect the remaining null bytes.
// https://github.com/vbatts/tar-split/blob/80a436fd6164c557b131f7c59ed69bd81af69761/concept/main.go#L49-L53
if _, err := dst.Write(remain); err != nil {
return err
}
}
}
break
}
if err != nil {
return fmt.Errorf("error reading from source tar: tar.Reader.Next: %v", err)
}
if cleanEntryName(h.Name) == TOCTarName {
// It is possible for a layer to be "stargzified" twice during the
// distribution lifecycle. So we reserve "TOCTarName" here to avoid
// duplicated entries in the resulting layer.
if lossless {
// We cannot handle this in lossless way.
return fmt.Errorf("existing TOC JSON is not allowed; decompress layer before append")
}
continue
}
xattrs := make(map[string][]byte)
const xattrPAXRecordsPrefix = "SCHILY.xattr."
if h.PAXRecords != nil {
for k, v := range h.PAXRecords {
if strings.HasPrefix(k, xattrPAXRecordsPrefix) {
xattrs[k[len(xattrPAXRecordsPrefix):]] = []byte(v)
}
}
}
ent := &TOCEntry{
Name: h.Name,
Mode: h.Mode,
UID: h.Uid,
GID: h.Gid,
Uname: w.nameIfChanged(&w.lastUsername, h.Uid, h.Uname),
Gname: w.nameIfChanged(&w.lastGroupname, h.Gid, h.Gname),
ModTime3339: formatModtime(h.ModTime),
Xattrs: xattrs,
}
if err := w.condOpenGz(); err != nil {
return err
}
if tw != nil {
if err := tw.WriteHeader(h); err != nil {
return err
}
} else {
if _, err := dst.Write(tr.RawBytes()); err != nil {
return err
}
}
switch h.Typeflag {
case tar.TypeLink:
ent.Type = "hardlink"
ent.LinkName = h.Linkname
case tar.TypeSymlink:
ent.Type = "symlink"
ent.LinkName = h.Linkname
case tar.TypeDir:
ent.Type = "dir"
case tar.TypeReg:
ent.Type = "reg"
ent.Size = h.Size
case tar.TypeChar:
ent.Type = "char"
ent.DevMajor = int(h.Devmajor)
ent.DevMinor = int(h.Devminor)
case tar.TypeBlock:
ent.Type = "block"
ent.DevMajor = int(h.Devmajor)
ent.DevMinor = int(h.Devminor)
case tar.TypeFifo:
ent.Type = "fifo"
default:
return fmt.Errorf("unsupported input tar entry %q", h.Typeflag)
}
// We need to keep a reference to the TOC entry for regular files, so that we
// can fill the digest later.
var regFileEntry *TOCEntry
var payloadDigest digest.Digester
if h.Typeflag == tar.TypeReg {
regFileEntry = ent
payloadDigest = digest.Canonical.Digester()
}
if h.Typeflag == tar.TypeReg && ent.Size > 0 {
var written int64
totalSize := ent.Size // save it before we destroy ent
tee := io.TeeReader(tr, payloadDigest.Hash())
for written < totalSize {
chunkSize := int64(w.chunkSize())
remain := totalSize - written
if remain < chunkSize {
chunkSize = remain
} else {
ent.ChunkSize = chunkSize
}
// We flush the underlying compression writer here to correctly calculate "w.cw.n".
if err := w.flushGz(); err != nil {
return err
}
if w.needsOpenGz(ent) || w.cw.n-prevOffset >= int64(w.MinChunkSize) {
if err := w.closeGz(); err != nil {
return err
}
ent.Offset = w.cw.n
prevOffset = ent.Offset
prevOffsetUncompressed = w.uncompressedCounter.n
} else {
ent.Offset = prevOffset
ent.InnerOffset = w.uncompressedCounter.n - prevOffsetUncompressed
}
ent.ChunkOffset = written
chunkDigest := digest.Canonical.Digester()
if err := w.condOpenGz(); err != nil {
return err
}
teeChunk := io.TeeReader(tee, chunkDigest.Hash())
var out io.Writer
if tw != nil {
out = tw
} else {
out = dst
}
if _, err := io.CopyN(out, teeChunk, chunkSize); err != nil {
return fmt.Errorf("error copying %q: %v", h.Name, err)
}
ent.ChunkDigest = chunkDigest.Digest().String()
w.toc.Entries = append(w.toc.Entries, ent)
written += chunkSize
ent = &TOCEntry{
Name: h.Name,
Type: "chunk",
}
}
} else {
w.toc.Entries = append(w.toc.Entries, ent)
}
if payloadDigest != nil {
regFileEntry.Digest = payloadDigest.Digest().String()
}
if tw != nil {
if err := tw.Flush(); err != nil {
return err
}
}
}
remainDest := io.Discard
if lossless {
remainDest = dst // Preserve the remaining bytes in lossless mode
}
_, err := io.Copy(remainDest, src)
return err
}
func (w *Writer) needsOpenGz(ent *TOCEntry) bool {
if ent.Type != "reg" {
return false
}
if w.needsOpenGzEntries == nil {
return false
}
_, ok := w.needsOpenGzEntries[ent.Name]
return ok
}
// DiffID returns the SHA-256 of the uncompressed tar bytes.
// It is only valid to call DiffID after Close.
func (w *Writer) DiffID() string {
return fmt.Sprintf("sha256:%x", w.diffHash.Sum(nil))
}
func maxFooterSize(blobSize int64, decompressors ...Decompressor) (res int64) {
for _, d := range decompressors {
if s := d.FooterSize(); res < s && s <= blobSize {
res = s
}
}
return
}
func parseTOC(d Decompressor, sr *io.SectionReader, tocOff, tocSize int64, tocBytes []byte, opts openOpts) (*Reader, error) {
if tocOff < 0 {
// This means that TOC isn't contained in the blob.
// We pass nil reader to ParseTOC and expect that ParseTOC acquire TOC from
// the external location.
start := time.Now()
toc, tocDgst, err := d.ParseTOC(nil)
if err != nil {
return nil, err
}
if opts.telemetry != nil && opts.telemetry.GetTocLatency != nil {
opts.telemetry.GetTocLatency(start)
}
if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil {
opts.telemetry.DeserializeTocLatency(start)
}
return &Reader{
sr: sr,
toc: toc,
tocDigest: tocDgst,
decompressor: d,
}, nil
}
if len(tocBytes) > 0 {
start := time.Now()
toc, tocDgst, err := d.ParseTOC(bytes.NewReader(tocBytes))
if err == nil {
if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil {
opts.telemetry.DeserializeTocLatency(start)
}
return &Reader{
sr: sr,
toc: toc,
tocDigest: tocDgst,
decompressor: d,
}, nil
}
}
start := time.Now()
tocBytes = make([]byte, tocSize)
if _, err := sr.ReadAt(tocBytes, tocOff); err != nil {
return nil, fmt.Errorf("error reading %d byte TOC targz: %v", len(tocBytes), err)
}
if opts.telemetry != nil && opts.telemetry.GetTocLatency != nil {
opts.telemetry.GetTocLatency(start)
}
start = time.Now()
toc, tocDgst, err := d.ParseTOC(bytes.NewReader(tocBytes))
if err != nil {
return nil, err
}
if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil {
opts.telemetry.DeserializeTocLatency(start)
}
return &Reader{
sr: sr,
toc: toc,
tocDigest: tocDgst,
decompressor: d,
}, nil
}
func formatModtime(t time.Time) string {
if t.IsZero() || t.Unix() == 0 {
return ""
}
return t.UTC().Round(time.Second).Format(time.RFC3339)
}
func cleanEntryName(name string) string {
// Use path.Clean to consistently deal with path separators across platforms.
return strings.TrimPrefix(path.Clean("/"+name), "/")
}
// countWriter counts how many bytes have been written to its wrapped
// io.Writer.
type countWriter struct {
w io.Writer
n int64
}
func (cw *countWriter) Write(p []byte) (n int, err error) {
n, err = cw.w.Write(p)
cw.n += int64(n)
return
}
type countWriteFlusher struct {
io.WriteCloser
n int64
}
func (wc *countWriteFlusher) register(w io.WriteCloser) io.WriteCloser {
wc.WriteCloser = w
return wc
}
func (wc *countWriteFlusher) Write(p []byte) (n int, err error) {
n, err = wc.WriteCloser.Write(p)
wc.n += int64(n)
return
}
func (wc *countWriteFlusher) Flush() error {
if f, ok := wc.WriteCloser.(interface {
Flush() error
}); ok {
return f.Flush()
}
return nil
}
func (wc *countWriteFlusher) Close() error {
err := wc.WriteCloser.Close()
wc.WriteCloser = nil
return err
}
// isGzip reports whether br is positioned right before an upcoming gzip stream.
// It does not consume any bytes from br.
func isGzip(br *bufio.Reader) bool {
const (
gzipID1 = 0x1f
gzipID2 = 0x8b
gzipDeflate = 8
)
peek, _ := br.Peek(3)
return len(peek) >= 3 && peek[0] == gzipID1 && peek[1] == gzipID2 && peek[2] == gzipDeflate
}
func positive(n int64) int64 {
if n < 0 {
return 0
}
return n
}
type countReader struct {
r io.Reader
n int64
}
func (cr *countReader) Read(p []byte) (n int, err error) {
n, err = cr.r.Read(p)
cr.n += int64(n)
return
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/gzip.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
package estargz
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/binary"
"encoding/json"
"fmt"
"hash"
"io"
"strconv"
digest "github.com/opencontainers/go-digest"
)
type gzipCompression struct {
*GzipCompressor
*GzipDecompressor
}
func newGzipCompressionWithLevel(level int) Compression {
return &gzipCompression{
&GzipCompressor{level},
&GzipDecompressor{},
}
}
func NewGzipCompressor() *GzipCompressor {
return &GzipCompressor{gzip.BestCompression}
}
func NewGzipCompressorWithLevel(level int) *GzipCompressor {
return &GzipCompressor{level}
}
type GzipCompressor struct {
compressionLevel int
}
func (gc *GzipCompressor) Writer(w io.Writer) (WriteFlushCloser, error) {
return gzip.NewWriterLevel(w, gc.compressionLevel)
}
func (gc *GzipCompressor) WriteTOCAndFooter(w io.Writer, off int64, toc *JTOC, diffHash hash.Hash) (digest.Digest, error) {
tocJSON, err := json.MarshalIndent(toc, "", "\t")
if err != nil {
return "", err
}
gz, _ := gzip.NewWriterLevel(w, gc.compressionLevel)
gw := io.Writer(gz)
if diffHash != nil {
gw = io.MultiWriter(gz, diffHash)
}
tw := tar.NewWriter(gw)
if err := tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: TOCTarName,
Size: int64(len(tocJSON)),
}); err != nil {
return "", err
}
if _, err := tw.Write(tocJSON); err != nil {
return "", err
}
if err := tw.Close(); err != nil {
return "", err
}
if err := gz.Close(); err != nil {
return "", err
}
if _, err := w.Write(gzipFooterBytes(off)); err != nil {
return "", err
}
return digest.FromBytes(tocJSON), nil
}
// gzipFooterBytes returns the 51 bytes footer.
func gzipFooterBytes(tocOff int64) []byte {
buf := bytes.NewBuffer(make([]byte, 0, FooterSize))
gz, _ := gzip.NewWriterLevel(buf, gzip.NoCompression) // MUST be NoCompression to keep 51 bytes
// Extra header indicating the offset of TOCJSON
// https://tools.ietf.org/html/rfc1952#section-2.3.1.1
header := make([]byte, 4)
header[0], header[1] = 'S', 'G'
subfield := fmt.Sprintf("%016xSTARGZ", tocOff)
binary.LittleEndian.PutUint16(header[2:4], uint16(len(subfield))) // little-endian per RFC1952
gz.Extra = append(header, []byte(subfield)...)
gz.Close()
if buf.Len() != FooterSize {
panic(fmt.Sprintf("footer buffer = %d, not %d", buf.Len(), FooterSize))
}
return buf.Bytes()
}
type GzipDecompressor struct{}
func (gz *GzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) {
return gzip.NewReader(r)
}
func (gz *GzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
return parseTOCEStargz(r)
}
func (gz *GzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) {
if len(p) != FooterSize {
return 0, 0, 0, fmt.Errorf("invalid length %d cannot be parsed", len(p))
}
zr, err := gzip.NewReader(bytes.NewReader(p))
if err != nil {
return 0, 0, 0, err
}
defer zr.Close()
extra := zr.Extra
si1, si2, subfieldlen, subfield := extra[0], extra[1], extra[2:4], extra[4:]
if si1 != 'S' || si2 != 'G' {
return 0, 0, 0, fmt.Errorf("invalid subfield IDs: %q, %q; want E, S", si1, si2)
}
if slen := binary.LittleEndian.Uint16(subfieldlen); slen != uint16(16+len("STARGZ")) {
return 0, 0, 0, fmt.Errorf("invalid length of subfield %d; want %d", slen, 16+len("STARGZ"))
}
if string(subfield[16:]) != "STARGZ" {
return 0, 0, 0, fmt.Errorf("STARGZ magic string must be included in the footer subfield")
}
tocOffset, err = strconv.ParseInt(string(subfield[:16]), 16, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err)
}
return tocOffset, tocOffset, 0, nil
}
func (gz *GzipDecompressor) FooterSize() int64 {
return FooterSize
}
func (gz *GzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) {
return decompressTOCEStargz(r)
}
type LegacyGzipDecompressor struct{}
func (gz *LegacyGzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) {
return gzip.NewReader(r)
}
func (gz *LegacyGzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
return parseTOCEStargz(r)
}
func (gz *LegacyGzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) {
if len(p) != legacyFooterSize {
return 0, 0, 0, fmt.Errorf("legacy: invalid length %d cannot be parsed", len(p))
}
zr, err := gzip.NewReader(bytes.NewReader(p))
if err != nil {
return 0, 0, 0, fmt.Errorf("legacy: failed to get footer gzip reader: %w", err)
}
defer zr.Close()
extra := zr.Extra
if len(extra) != 16+len("STARGZ") {
return 0, 0, 0, fmt.Errorf("legacy: invalid stargz's extra field size")
}
if string(extra[16:]) != "STARGZ" {
return 0, 0, 0, fmt.Errorf("legacy: magic string STARGZ not found")
}
tocOffset, err = strconv.ParseInt(string(extra[:16]), 16, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err)
}
return tocOffset, tocOffset, 0, nil
}
func (gz *LegacyGzipDecompressor) FooterSize() int64 {
return legacyFooterSize
}
func (gz *LegacyGzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) {
return decompressTOCEStargz(r)
}
func parseTOCEStargz(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
tr, err := decompressTOCEStargz(r)
if err != nil {
return nil, "", err
}
dgstr := digest.Canonical.Digester()
toc = new(JTOC)
if err := json.NewDecoder(io.TeeReader(tr, dgstr.Hash())).Decode(&toc); err != nil {
return nil, "", fmt.Errorf("error decoding TOC JSON: %v", err)
}
if err := tr.Close(); err != nil {
return nil, "", err
}
return toc, dgstr.Digest(), nil
}
func decompressTOCEStargz(r io.Reader) (tocJSON io.ReadCloser, err error) {
zr, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("malformed TOC gzip header: %v", err)
}
zr.Multistream(false)
tr := tar.NewReader(zr)
h, err := tr.Next()
if err != nil {
return nil, fmt.Errorf("failed to find tar header in TOC gzip stream: %v", err)
}
if h.Name != TOCTarName {
return nil, fmt.Errorf("TOC tar entry had name %q; expected %q", h.Name, TOCTarName)
}
return readCloser{tr, zr.Close}, nil
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/testutil.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
package estargz
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
"github.com/klauspost/compress/zstd"
digest "github.com/opencontainers/go-digest"
)
// TestingController is Compression with some helper methods necessary for testing.
type TestingController interface {
Compression
TestStreams(t TestingT, b []byte, streams []int64)
DiffIDOf(TestingT, []byte) string
String() string
}
// TestingT is the minimal set of testing.T required to run the
// tests defined in CompressionTestSuite. This interface exists to prevent
// leaking the testing package from being exposed outside tests.
type TestingT interface {
Errorf(format string, args ...any)
FailNow()
Failed() bool
Fatal(args ...any)
Fatalf(format string, args ...any)
Logf(format string, args ...any)
Parallel()
}
// Runner allows running subtests of TestingT. This exists instead of adding
// a Run method to TestingT interface because the Run implementation of
// testing.T would not satisfy the interface.
type Runner func(t TestingT, name string, fn func(t TestingT))
type TestRunner struct {
TestingT
Runner Runner
}
func (r *TestRunner) Run(name string, run func(*TestRunner)) {
r.Runner(r.TestingT, name, func(t TestingT) {
run(&TestRunner{TestingT: t, Runner: r.Runner})
})
}
// CompressionTestSuite tests this pkg with controllers can build valid eStargz blobs and parse them.
func CompressionTestSuite(t *TestRunner, controllers ...TestingControllerFactory) {
t.Run("testBuild", func(t *TestRunner) { t.Parallel(); testBuild(t, controllers...) })
t.Run("testDigestAndVerify", func(t *TestRunner) {
t.Parallel()
testDigestAndVerify(t, controllers...)
})
t.Run("testWriteAndOpen", func(t *TestRunner) { t.Parallel(); testWriteAndOpen(t, controllers...) })
}
type TestingControllerFactory func() TestingController
const (
uncompressedType int = iota
gzipType
zstdType
)
var srcCompressions = []int{
uncompressedType,
gzipType,
zstdType,
}
var allowedPrefix = [4]string{"", "./", "/", "../"}
// testBuild tests the resulting stargz blob built by this pkg has the same
// contents as the normal stargz blob.
func testBuild(t *TestRunner, controllers ...TestingControllerFactory) {
tests := []struct {
name string
chunkSize int
minChunkSize []int
in []tarEntry
}{
{
name: "regfiles and directories",
chunkSize: 4,
in: tarOf(
file("foo", "test1"),
dir("foo2/"),
file("foo2/bar", "test2", xAttr(map[string]string{"test": "sample"})),
),
},
{
name: "empty files",
chunkSize: 4,
in: tarOf(
file("foo", "tttttt"),
file("foo_empty", ""),
file("foo2", "tttttt"),
file("foo_empty2", ""),
file("foo3", "tttttt"),
file("foo_empty3", ""),
file("foo4", "tttttt"),
file("foo_empty4", ""),
file("foo5", "tttttt"),
file("foo_empty5", ""),
file("foo6", "tttttt"),
),
},
{
name: "various files",
chunkSize: 4,
minChunkSize: []int{0, 64000},
in: tarOf(
file("baz.txt", "bazbazbazbazbazbazbaz"),
file("foo1.txt", "a"),
file("bar/foo2.txt", "b"),
file("foo3.txt", "c"),
symlink("barlink", "test/bar.txt"),
dir("test/"),
dir("dev/"),
blockdev("dev/testblock", 3, 4),
fifo("dev/testfifo"),
chardev("dev/testchar1", 5, 6),
file("test/bar.txt", "testbartestbar", xAttr(map[string]string{"test2": "sample2"})),
dir("test2/"),
link("test2/bazlink", "baz.txt"),
chardev("dev/testchar2", 1, 2),
),
},
{
name: "no contents",
chunkSize: 4,
in: tarOf(
file("baz.txt", ""),
symlink("barlink", "test/bar.txt"),
dir("test/"),
dir("dev/"),
blockdev("dev/testblock", 3, 4),
fifo("dev/testfifo"),
chardev("dev/testchar1", 5, 6),
file("test/bar.txt", "", xAttr(map[string]string{"test2": "sample2"})),
dir("test2/"),
link("test2/bazlink", "baz.txt"),
chardev("dev/testchar2", 1, 2),
),
},
}
for _, tt := range tests {
if len(tt.minChunkSize) == 0 {
tt.minChunkSize = []int{0}
}
for _, srcCompression := range srcCompressions {
srcCompression := srcCompression
for _, newCL := range controllers {
newCL := newCL
for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} {
srcTarFormat := srcTarFormat
for _, prefix := range allowedPrefix {
prefix := prefix
for _, minChunkSize := range tt.minChunkSize {
minChunkSize := minChunkSize
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,src=%d,format=%s,minChunkSize=%d", newCL(), prefix, srcCompression, srcTarFormat, minChunkSize), func(t *TestRunner) {
tarBlob := buildTar(t, tt.in, prefix, srcTarFormat)
// Test divideEntries()
entries, err := sortEntries(tarBlob, nil, nil) // identical order
if err != nil {
t.Fatalf("failed to parse tar: %v", err)
}
var merged []*entry
for _, part := range divideEntries(entries, 4) {
merged = append(merged, part...)
}
if !reflect.DeepEqual(entries, merged) {
for _, e := range entries {
t.Logf("Original: %v", e.header)
}
for _, e := range merged {
t.Logf("Merged: %v", e.header)
}
t.Errorf("divided entries couldn't be merged")
return
}
// Prepare sample data
cl1 := newCL()
wantBuf := new(bytes.Buffer)
sw := NewWriterWithCompressor(wantBuf, cl1)
sw.MinChunkSize = minChunkSize
sw.ChunkSize = tt.chunkSize
if err := sw.AppendTar(tarBlob); err != nil {
t.Fatalf("failed to append tar to want stargz: %v", err)
}
if _, err := sw.Close(); err != nil {
t.Fatalf("failed to prepare want stargz: %v", err)
}
wantData := wantBuf.Bytes()
want, err := Open(io.NewSectionReader(
bytes.NewReader(wantData), 0, int64(len(wantData))),
WithDecompressors(cl1),
)
if err != nil {
t.Fatalf("failed to parse the want stargz: %v", err)
}
// Prepare testing data
var opts []Option
if minChunkSize > 0 {
opts = append(opts, WithMinChunkSize(minChunkSize))
}
cl2 := newCL()
rc, err := Build(compressBlob(t, tarBlob, srcCompression),
append(opts, WithChunkSize(tt.chunkSize), WithCompression(cl2))...)
if err != nil {
t.Fatalf("failed to build stargz: %v", err)
}
defer rc.Close()
gotBuf := new(bytes.Buffer)
if _, err := io.Copy(gotBuf, rc); err != nil {
t.Fatalf("failed to copy built stargz blob: %v", err)
}
gotData := gotBuf.Bytes()
got, err := Open(io.NewSectionReader(
bytes.NewReader(gotBuf.Bytes()), 0, int64(len(gotData))),
WithDecompressors(cl2),
)
if err != nil {
t.Fatalf("failed to parse the got stargz: %v", err)
}
// Check DiffID is properly calculated
rc.Close()
diffID := rc.DiffID()
wantDiffID := cl2.DiffIDOf(t, gotData)
if diffID.String() != wantDiffID {
t.Errorf("DiffID = %q; want %q", diffID, wantDiffID)
}
// Compare as stargz
if !isSameVersion(t, cl1, wantData, cl2, gotData) {
t.Errorf("built stargz hasn't same json")
return
}
if !isSameEntries(t, want, got) {
t.Errorf("built stargz isn't same as the original")
return
}
// Compare as tar.gz
if !isSameTarGz(t, cl1, wantData, cl2, gotData) {
t.Errorf("built stargz isn't same tar.gz")
return
}
})
}
}
}
}
}
}
}
func isSameTarGz(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool {
aGz, err := cla.Reader(bytes.NewReader(a))
if err != nil {
t.Fatalf("failed to read A")
}
defer aGz.Close()
bGz, err := clb.Reader(bytes.NewReader(b))
if err != nil {
t.Fatalf("failed to read B")
}
defer bGz.Close()
// Same as tar's Next() method but ignores landmarks and TOCJSON file
next := func(r *tar.Reader) (h *tar.Header, err error) {
for {
if h, err = r.Next(); err != nil {
return
}
if h.Name != PrefetchLandmark &&
h.Name != NoPrefetchLandmark &&
h.Name != TOCTarName {
return
}
}
}
aTar := tar.NewReader(aGz)
bTar := tar.NewReader(bGz)
for {
// Fetch and parse next header.
aH, aErr := next(aTar)
bH, bErr := next(bTar)
if aErr != nil || bErr != nil {
if aErr == io.EOF && bErr == io.EOF {
break
}
t.Fatalf("Failed to parse tar file: A: %v, B: %v", aErr, bErr)
}
if !reflect.DeepEqual(aH, bH) {
t.Logf("different header (A = %v; B = %v)", aH, bH)
return false
}
aFile, err := io.ReadAll(aTar)
if err != nil {
t.Fatal("failed to read tar payload of A")
}
bFile, err := io.ReadAll(bTar)
if err != nil {
t.Fatal("failed to read tar payload of B")
}
if !bytes.Equal(aFile, bFile) {
t.Logf("different tar payload (A = %q; B = %q)", string(a), string(b))
return false
}
}
return true
}
func isSameVersion(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool {
aJTOC, _, err := parseStargz(io.NewSectionReader(bytes.NewReader(a), 0, int64(len(a))), cla)
if err != nil {
t.Fatalf("failed to parse A: %v", err)
}
bJTOC, _, err := parseStargz(io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))), clb)
if err != nil {
t.Fatalf("failed to parse B: %v", err)
}
t.Logf("A: TOCJSON: %v", dumpTOCJSON(t, aJTOC))
t.Logf("B: TOCJSON: %v", dumpTOCJSON(t, bJTOC))
return aJTOC.Version == bJTOC.Version
}
func isSameEntries(t TestingT, a, b *Reader) bool {
aroot, ok := a.Lookup("")
if !ok {
t.Fatalf("failed to get root of A")
}
broot, ok := b.Lookup("")
if !ok {
t.Fatalf("failed to get root of B")
}
aEntry := stargzEntry{aroot, a}
bEntry := stargzEntry{broot, b}
return contains(t, aEntry, bEntry) && contains(t, bEntry, aEntry)
}
func compressBlob(t TestingT, src *io.SectionReader, srcCompression int) *io.SectionReader {
buf := new(bytes.Buffer)
var w io.WriteCloser
var err error
switch srcCompression {
case gzipType:
w = gzip.NewWriter(buf)
case zstdType:
w, err = zstd.NewWriter(buf)
if err != nil {
t.Fatalf("failed to init zstd writer: %v", err)
}
default:
return src
}
src.Seek(0, io.SeekStart)
if _, err := io.Copy(w, src); err != nil {
t.Fatalf("failed to compress source")
}
if err := w.Close(); err != nil {
t.Fatalf("failed to finalize compress source")
}
data := buf.Bytes()
return io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data)))
}
type stargzEntry struct {
e *TOCEntry
r *Reader
}
// contains checks if all child entries in "b" are also contained in "a".
// This function also checks if the files/chunks contain the same contents among "a" and "b".
func contains(t TestingT, a, b stargzEntry) bool {
ae, ar := a.e, a.r
be, br := b.e, b.r
t.Logf("Comparing: %q vs %q", ae.Name, be.Name)
if !equalEntry(ae, be) {
t.Logf("%q != %q: entry: a: %v, b: %v", ae.Name, be.Name, ae, be)
return false
}
if ae.Type == "dir" {
t.Logf("Directory: %q vs %q: %v vs %v", ae.Name, be.Name,
allChildrenName(ae), allChildrenName(be))
iscontain := true
ae.ForeachChild(func(aBaseName string, aChild *TOCEntry) bool {
// Walk through all files on this stargz file.
if aChild.Name == PrefetchLandmark ||
aChild.Name == NoPrefetchLandmark {
return true // Ignore landmarks
}
// Ignore a TOCEntry of "./" (formated as "" by stargz lib) on root directory
// because this points to the root directory itself.
if aChild.Name == "" && ae.Name == "" {
return true
}
bChild, ok := be.LookupChild(aBaseName)
if !ok {
t.Logf("%q (base: %q): not found in b: %v",
ae.Name, aBaseName, allChildrenName(be))
iscontain = false
return false
}
childcontain := contains(t, stargzEntry{aChild, a.r}, stargzEntry{bChild, b.r})
if !childcontain {
t.Logf("%q != %q: non-equal dir", ae.Name, be.Name)
iscontain = false
return false
}
return true
})
return iscontain
} else if ae.Type == "reg" {
af, err := ar.OpenFile(ae.Name)
if err != nil {
t.Fatalf("failed to open file %q on A: %v", ae.Name, err)
}
bf, err := br.OpenFile(be.Name)
if err != nil {
t.Fatalf("failed to open file %q on B: %v", be.Name, err)
}
var nr int64
for nr < ae.Size {
abytes, anext, aok := readOffset(t, af, nr, a)
bbytes, bnext, bok := readOffset(t, bf, nr, b)
if !aok && !bok {
break
} else if !aok || !bok || anext != bnext {
t.Logf("%q != %q (offset=%d): chunk existence a=%v vs b=%v, anext=%v vs bnext=%v",
ae.Name, be.Name, nr, aok, bok, anext, bnext)
return false
}
nr = anext
if !bytes.Equal(abytes, bbytes) {
t.Logf("%q != %q: different contents %v vs %v",
ae.Name, be.Name, string(abytes), string(bbytes))
return false
}
}
return true
}
return true
}
func allChildrenName(e *TOCEntry) (children []string) {
e.ForeachChild(func(baseName string, _ *TOCEntry) bool {
children = append(children, baseName)
return true
})
return
}
func equalEntry(a, b *TOCEntry) bool {
// Here, we selectively compare fileds that we are interested in.
return a.Name == b.Name &&
a.Type == b.Type &&
a.Size == b.Size &&
a.ModTime3339 == b.ModTime3339 &&
a.Stat().ModTime().Equal(b.Stat().ModTime()) && // modTime time.Time
a.LinkName == b.LinkName &&
a.Mode == b.Mode &&
a.UID == b.UID &&
a.GID == b.GID &&
a.Uname == b.Uname &&
a.Gname == b.Gname &&
(a.Offset >= 0) == (b.Offset >= 0) &&
(a.NextOffset() > 0) == (b.NextOffset() > 0) &&
a.DevMajor == b.DevMajor &&
a.DevMinor == b.DevMinor &&
a.NumLink == b.NumLink &&
reflect.DeepEqual(a.Xattrs, b.Xattrs) &&
// chunk-related infomations aren't compared in this function.
// ChunkOffset int64 `json:"chunkOffset,omitempty"`
// ChunkSize int64 `json:"chunkSize,omitempty"`
// children map[string]*TOCEntry
a.Digest == b.Digest
}
func readOffset(t TestingT, r *io.SectionReader, offset int64, e stargzEntry) ([]byte, int64, bool) {
ce, ok := e.r.ChunkEntryForOffset(e.e.Name, offset)
if !ok {
return nil, 0, false
}
data := make([]byte, ce.ChunkSize)
t.Logf("Offset: %v, NextOffset: %v", ce.Offset, ce.NextOffset())
n, err := r.ReadAt(data, ce.ChunkOffset)
if err != nil {
t.Fatalf("failed to read file payload of %q (offset:%d,size:%d): %v",
e.e.Name, ce.ChunkOffset, ce.ChunkSize, err)
}
if int64(n) != ce.ChunkSize {
t.Fatalf("unexpected copied data size %d; want %d",
n, ce.ChunkSize)
}
return data[:n], offset + ce.ChunkSize, true
}
func dumpTOCJSON(t TestingT, tocJSON *JTOC) string {
jtocData, err := json.Marshal(*tocJSON)
if err != nil {
t.Fatalf("failed to marshal TOC JSON: %v", err)
}
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, bytes.NewReader(jtocData)); err != nil {
t.Fatalf("failed to read toc json blob: %v", err)
}
return buf.String()
}
const chunkSize = 3
type check func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory)
// testDigestAndVerify runs specified checks against sample stargz blobs.
func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory) {
tests := []struct {
name string
tarInit func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry)
checks []check
minChunkSize []int
}{
{
name: "no-regfile",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
dir("test/"),
)
},
checks: []check{
checkStargzTOC,
checkVerifyTOC,
checkVerifyInvalidStargzFail(buildTar(t, tarOf(
dir("test2/"), // modified
), allowedPrefix[0])),
},
},
{
name: "small-files",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
dir("test/"),
regDigest(t, "test/bar.txt", "bbb", dgstMap),
)
},
minChunkSize: []int{0, 64000},
checks: []check{
checkStargzTOC,
checkVerifyTOC,
checkVerifyInvalidStargzFail(buildTar(t, tarOf(
file("baz.txt", ""),
file("foo.txt", "M"), // modified
dir("test/"),
file("test/bar.txt", "bbb"),
), allowedPrefix[0])),
// checkVerifyInvalidTOCEntryFail("foo.txt"), // TODO
checkVerifyBrokenContentFail("foo.txt"),
},
},
{
name: "big-files",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
dir("test/"),
regDigest(t, "test/bar.txt", "testbartestbar", dgstMap),
)
},
checks: []check{
checkStargzTOC,
checkVerifyTOC,
checkVerifyInvalidStargzFail(buildTar(t, tarOf(
file("baz.txt", "bazbazbazMMMbazbazbaz"), // modified
file("foo.txt", "a"),
dir("test/"),
file("test/bar.txt", "testbartestbar"),
), allowedPrefix[0])),
checkVerifyInvalidTOCEntryFail("test/bar.txt"),
checkVerifyBrokenContentFail("test/bar.txt"),
},
},
{
name: "with-non-regfiles",
minChunkSize: []int{0, 64000},
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
regDigest(t, "bar/foo2.txt", "b", dgstMap),
regDigest(t, "foo3.txt", "c", dgstMap),
symlink("barlink", "test/bar.txt"),
dir("test/"),
regDigest(t, "test/bar.txt", "testbartestbar", dgstMap),
dir("test2/"),
link("test2/bazlink", "baz.txt"),
)
},
checks: []check{
checkStargzTOC,
checkVerifyTOC,
checkVerifyInvalidStargzFail(buildTar(t, tarOf(
file("baz.txt", "bazbazbazbazbazbazbaz"),
file("foo.txt", "a"),
file("bar/foo2.txt", "b"),
file("foo3.txt", "c"),
symlink("barlink", "test/bar.txt"),
dir("test/"),
file("test/bar.txt", "testbartestbar"),
dir("test2/"),
link("test2/bazlink", "foo.txt"), // modified
), allowedPrefix[0])),
checkVerifyInvalidTOCEntryFail("test/bar.txt"),
checkVerifyBrokenContentFail("test/bar.txt"),
},
},
}
for _, tt := range tests {
if len(tt.minChunkSize) == 0 {
tt.minChunkSize = []int{0}
}
for _, srcCompression := range srcCompressions {
srcCompression := srcCompression
for _, newCL := range controllers {
newCL := newCL
for _, prefix := range allowedPrefix {
prefix := prefix
for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} {
srcTarFormat := srcTarFormat
for _, minChunkSize := range tt.minChunkSize {
minChunkSize := minChunkSize
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,format=%s,minChunkSize=%d", newCL(), prefix, srcTarFormat, minChunkSize), func(t *TestRunner) {
// Get original tar file and chunk digests
dgstMap := make(map[string]digest.Digest)
tarBlob := buildTar(t, tt.tarInit(t, dgstMap), prefix, srcTarFormat)
cl := newCL()
rc, err := Build(compressBlob(t, tarBlob, srcCompression),
WithChunkSize(chunkSize), WithCompression(cl))
if err != nil {
t.Fatalf("failed to convert stargz: %v", err)
}
tocDigest := rc.TOCDigest()
defer rc.Close()
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, rc); err != nil {
t.Fatalf("failed to copy built stargz blob: %v", err)
}
newStargz := buf.Bytes()
// NoPrefetchLandmark is added during `Bulid`, which is expected behaviour.
dgstMap[chunkID(NoPrefetchLandmark, 0, int64(len([]byte{landmarkContents})))] = digest.FromBytes([]byte{landmarkContents})
for _, check := range tt.checks {
check(t, newStargz, tocDigest, dgstMap, cl, newCL)
}
})
}
}
}
}
}
}
}
// checkStargzTOC checks the TOC JSON of the passed stargz has the expected
// digest and contains valid chunks. It walks all entries in the stargz and
// checks all chunk digests stored to the TOC JSON match the actual contents.
func checkStargzTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
WithDecompressors(controller),
)
if err != nil {
t.Errorf("failed to parse converted stargz: %v", err)
return
}
digestMapTOC, err := listDigests(io.NewSectionReader(
bytes.NewReader(sgzData), 0, int64(len(sgzData))),
controller,
)
if err != nil {
t.Fatalf("failed to list digest: %v", err)
}
found := make(map[string]bool)
for id := range dgstMap {
found[id] = false
}
zr, err := controller.Reader(bytes.NewReader(sgzData))
if err != nil {
t.Fatalf("failed to decompress converted stargz: %v", err)
}
defer zr.Close()
tr := tar.NewReader(zr)
for {
h, err := tr.Next()
if err != nil {
if err != io.EOF {
t.Errorf("failed to read tar entry: %v", err)
return
}
break
}
if h.Name == TOCTarName {
// Check the digest of TOC JSON based on the actual contents
// It's sure that TOC JSON exists in this archive because
// Open succeeded.
dgstr := digest.Canonical.Digester()
if _, err := io.Copy(dgstr.Hash(), tr); err != nil {
t.Fatalf("failed to calculate digest of TOC JSON: %v",
err)
}
if dgstr.Digest() != tocDigest {
t.Errorf("invalid TOC JSON %q; want %q", tocDigest, dgstr.Digest())
}
continue
}
if _, ok := sgz.Lookup(h.Name); !ok {
t.Errorf("lost stargz entry %q in the converted TOC", h.Name)
return
}
var n int64
for n < h.Size {
ce, ok := sgz.ChunkEntryForOffset(h.Name, n)
if !ok {
t.Errorf("lost chunk %q(offset=%d) in the converted TOC",
h.Name, n)
return
}
// Get the original digest to make sure the file contents are kept unchanged
// from the original tar, during the whole conversion steps.
id := chunkID(h.Name, n, ce.ChunkSize)
want, ok := dgstMap[id]
if !ok {
t.Errorf("Unexpected chunk %q(offset=%d,size=%d): %v",
h.Name, n, ce.ChunkSize, dgstMap)
return
}
found[id] = true
// Check the file contents
dgstr := digest.Canonical.Digester()
if _, err := io.CopyN(dgstr.Hash(), tr, ce.ChunkSize); err != nil {
t.Fatalf("failed to calculate digest of %q (offset=%d,size=%d)",
h.Name, n, ce.ChunkSize)
}
if want != dgstr.Digest() {
t.Errorf("Invalid contents in converted stargz %q: %q; want %q",
h.Name, dgstr.Digest(), want)
return
}
// Check the digest stored in TOC JSON
dgstTOC, ok := digestMapTOC[ce.Offset]
if !ok {
t.Errorf("digest of %q(offset=%d,size=%d,chunkOffset=%d) isn't registered",
h.Name, ce.Offset, ce.ChunkSize, ce.ChunkOffset)
}
if want != dgstTOC {
t.Errorf("Invalid digest in TOCEntry %q: %q; want %q",
h.Name, dgstTOC, want)
return
}
n += ce.ChunkSize
}
}
for id, ok := range found {
if !ok {
t.Errorf("required chunk %q not found in the converted stargz: %v", id, found)
}
}
}
// checkVerifyTOC checks the verification works for the TOC JSON of the passed
// stargz. It walks all entries in the stargz and checks the verifications for
// all chunks work.
func checkVerifyTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
WithDecompressors(controller),
)
if err != nil {
t.Errorf("failed to parse converted stargz: %v", err)
return
}
ev, err := sgz.VerifyTOC(tocDigest)
if err != nil {
t.Errorf("failed to verify stargz: %v", err)
return
}
found := make(map[string]bool)
for id := range dgstMap {
found[id] = false
}
zr, err := controller.Reader(bytes.NewReader(sgzData))
if err != nil {
t.Fatalf("failed to decompress converted stargz: %v", err)
}
defer zr.Close()
tr := tar.NewReader(zr)
for {
h, err := tr.Next()
if err != nil {
if err != io.EOF {
t.Errorf("failed to read tar entry: %v", err)
return
}
break
}
if h.Name == TOCTarName {
continue
}
if _, ok := sgz.Lookup(h.Name); !ok {
t.Errorf("lost stargz entry %q in the converted TOC", h.Name)
return
}
var n int64
for n < h.Size {
ce, ok := sgz.ChunkEntryForOffset(h.Name, n)
if !ok {
t.Errorf("lost chunk %q(offset=%d) in the converted TOC",
h.Name, n)
return
}
v, err := ev.Verifier(ce)
if err != nil {
t.Errorf("failed to get verifier for %q(offset=%d)", h.Name, n)
}
found[chunkID(h.Name, n, ce.ChunkSize)] = true
// Check the file contents
if _, err := io.CopyN(v, tr, ce.ChunkSize); err != nil {
t.Fatalf("failed to get chunk of %q (offset=%d,size=%d)",
h.Name, n, ce.ChunkSize)
}
if !v.Verified() {
t.Errorf("Invalid contents in converted stargz %q (should be succeeded)",
h.Name)
return
}
n += ce.ChunkSize
}
}
for id, ok := range found {
if !ok {
t.Errorf("required chunk %q not found in the converted stargz: %v", id, found)
}
}
}
// checkVerifyInvalidTOCEntryFail checks if misconfigured TOC JSON can be
// detected during the verification and the verification returns an error.
func checkVerifyInvalidTOCEntryFail(filename string) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
funcs := map[string]rewriteFunc{
"lost digest in a entry": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
var found bool
for _, e := range toc.Entries {
if cleanEntryName(e.Name) == filename {
if e.Type != "reg" && e.Type != "chunk" {
t.Fatalf("entry %q to break must be regfile or chunk", filename)
}
if e.ChunkDigest == "" {
t.Fatalf("entry %q is already invalid", filename)
}
e.ChunkDigest = ""
found = true
}
}
if !found {
t.Fatalf("rewrite target not found")
}
},
"duplicated entry offset": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
var (
sampleEntry *TOCEntry
targetEntry *TOCEntry
)
for _, e := range toc.Entries {
if e.Type == "reg" || e.Type == "chunk" {
if cleanEntryName(e.Name) == filename {
targetEntry = e
} else {
sampleEntry = e
}
}
}
if sampleEntry == nil {
t.Fatalf("TOC must contain at least one regfile or chunk entry other than the rewrite target")
return
}
if targetEntry == nil {
t.Fatalf("rewrite target not found")
return
}
targetEntry.Offset = sampleEntry.Offset
},
}
for name, rFunc := range funcs {
t.Run(name, func(t *TestRunner) {
newSgz, newTocDigest := rewriteTOCJSON(t, io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), rFunc, controller)
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, newSgz); err != nil {
t.Fatalf("failed to get converted stargz")
}
isgz := buf.Bytes()
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(isgz), 0, int64(len(isgz))),
WithDecompressors(controller),
)
if err != nil {
t.Fatalf("failed to parse converted stargz: %v", err)
return
}
_, err = sgz.VerifyTOC(newTocDigest)
if err == nil {
t.Errorf("must fail for invalid TOC")
return
}
})
}
}
}
// checkVerifyInvalidStargzFail checks if the verification detects that the
// given stargz file doesn't match to the expected digest and returns error.
func checkVerifyInvalidStargzFail(invalid *io.SectionReader) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
cl := newController()
rc, err := Build(invalid, WithChunkSize(chunkSize), WithCompression(cl))
if err != nil {
t.Fatalf("failed to convert stargz: %v", err)
}
defer rc.Close()
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, rc); err != nil {
t.Fatalf("failed to copy built stargz blob: %v", err)
}
mStargz := buf.Bytes()
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(mStargz), 0, int64(len(mStargz))),
WithDecompressors(cl),
)
if err != nil {
t.Fatalf("failed to parse converted stargz: %v", err)
return
}
_, err = sgz.VerifyTOC(tocDigest)
if err == nil {
t.Errorf("must fail for invalid TOC")
return
}
}
}
// checkVerifyBrokenContentFail checks if the verifier detects broken contents
// that doesn't match to the expected digest and returns error.
func checkVerifyBrokenContentFail(filename string) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
// Parse stargz file
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
WithDecompressors(controller),
)
if err != nil {
t.Fatalf("failed to parse converted stargz: %v", err)
return
}
ev, err := sgz.VerifyTOC(tocDigest)
if err != nil {
t.Fatalf("failed to verify stargz: %v", err)
return
}
// Open the target file
sr, err := sgz.OpenFile(filename)
if err != nil {
t.Fatalf("failed to open file %q", filename)
}
ce, ok := sgz.ChunkEntryForOffset(filename, 0)
if !ok {
t.Fatalf("lost chunk %q(offset=%d) in the converted TOC", filename, 0)
return
}
if ce.ChunkSize == 0 {
t.Fatalf("file mustn't be empty")
return
}
data := make([]byte, ce.ChunkSize)
if _, err := sr.ReadAt(data, ce.ChunkOffset); err != nil {
t.Errorf("failed to get data of a chunk of %q(offset=%q)",
filename, ce.ChunkOffset)
}
// Check the broken chunk (must fail)
v, err := ev.Verifier(ce)
if err != nil {
t.Fatalf("failed to get verifier for %q", filename)
}
broken := append([]byte{^data[0]}, data[1:]...)
if _, err := io.CopyN(v, bytes.NewReader(broken), ce.ChunkSize); err != nil {
t.Fatalf("failed to get chunk of %q (offset=%d,size=%d)",
filename, ce.ChunkOffset, ce.ChunkSize)
}
if v.Verified() {
t.Errorf("verification must fail for broken file chunk %q(org:%q,broken:%q)",
filename, data, broken)
}
}
}
func chunkID(name string, offset, size int64) string {
return fmt.Sprintf("%s-%d-%d", cleanEntryName(name), offset, size)
}
type rewriteFunc func(t TestingT, toc *JTOC, sgz *io.SectionReader)
func rewriteTOCJSON(t TestingT, sgz *io.SectionReader, rewrite rewriteFunc, controller TestingController) (newSgz io.Reader, tocDigest digest.Digest) {
decodedJTOC, jtocOffset, err := parseStargz(sgz, controller)
if err != nil {
t.Fatalf("failed to extract TOC JSON: %v", err)
}
rewrite(t, decodedJTOC, sgz)
tocFooter, tocDigest, err := tocAndFooter(controller, decodedJTOC, jtocOffset)
if err != nil {
t.Fatalf("failed to create toc and footer: %v", err)
}
// Reconstruct stargz file with the modified TOC JSON
if _, err := sgz.Seek(0, io.SeekStart); err != nil {
t.Fatalf("failed to reset the seek position of stargz: %v", err)
}
return io.MultiReader(
io.LimitReader(sgz, jtocOffset), // Original stargz (before TOC JSON)
tocFooter, // Rewritten TOC and footer
), tocDigest
}
func listDigests(sgz *io.SectionReader, controller TestingController) (map[int64]digest.Digest, error) {
decodedJTOC, _, err := parseStargz(sgz, controller)
if err != nil {
return nil, err
}
digestMap := make(map[int64]digest.Digest)
for _, e := range decodedJTOC.Entries {
if e.Type == "reg" || e.Type == "chunk" {
if e.Type == "reg" && e.Size == 0 {
continue // ignores empty file
}
if e.ChunkDigest == "" {
return nil, fmt.Errorf("ChunkDigest of %q(off=%d) not found in TOC JSON",
e.Name, e.Offset)
}
d, err := digest.Parse(e.ChunkDigest)
if err != nil {
return nil, err
}
digestMap[e.Offset] = d
}
}
return digestMap, nil
}
func parseStargz(sgz *io.SectionReader, controller TestingController) (decodedJTOC *JTOC, jtocOffset int64, err error) {
fSize := controller.FooterSize()
footer := make([]byte, fSize)
if _, err := sgz.ReadAt(footer, sgz.Size()-fSize); err != nil {
return nil, 0, fmt.Errorf("error reading footer: %w", err)
}
_, tocOffset, _, err := controller.ParseFooter(footer[positive(int64(len(footer))-fSize):])
if err != nil {
return nil, 0, fmt.Errorf("failed to parse footer: %w", err)
}
// Decode the TOC JSON
var tocReader io.Reader
if tocOffset >= 0 {
tocReader = io.NewSectionReader(sgz, tocOffset, sgz.Size()-tocOffset-fSize)
}
decodedJTOC, _, err = controller.ParseTOC(tocReader)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse TOC: %w", err)
}
return decodedJTOC, tocOffset, nil
}
func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) {
const content = "Some contents"
invalidUtf8 := "\xff\xfe\xfd"
xAttrFile := xAttr{"foo": "bar", "invalid-utf8": invalidUtf8}
sampleOwner := owner{uid: 50, gid: 100}
data64KB := randomContents(64000)
tests := []struct {
name string
chunkSize int
minChunkSize int
in []tarEntry
want []stargzCheck
wantNumGz int // expected number of streams
wantNumGzLossLess int // expected number of streams (> 0) in lossless mode if it's different from wantNumGz
wantFailOnLossLess bool
wantTOCVersion int // default = 1
}{
{
name: "empty",
in: tarOf(),
wantNumGz: 2, // (empty tar) + TOC + footer
want: checks(
numTOCEntries(0),
),
},
{
name: "1dir_1empty_file",
in: tarOf(
dir("foo/"),
file("foo/bar.txt", ""),
),
wantNumGz: 3, // dir, TOC, footer
want: checks(
numTOCEntries(2),
hasDir("foo/"),
hasFileLen("foo/bar.txt", 0),
entryHasChildren("foo", "bar.txt"),
hasFileDigest("foo/bar.txt", digestFor("")),
),
},
{
name: "1dir_1file",
in: tarOf(
dir("foo/"),
file("foo/bar.txt", content, xAttrFile),
),
wantNumGz: 4, // var dir, foo.txt alone, TOC, footer
want: checks(
numTOCEntries(2),
hasDir("foo/"),
hasFileLen("foo/bar.txt", len(content)),
hasFileDigest("foo/bar.txt", digestFor(content)),
hasFileContentsRange("foo/bar.txt", 0, content),
hasFileContentsRange("foo/bar.txt", 1, content[1:]),
entryHasChildren("", "foo"),
entryHasChildren("foo", "bar.txt"),
hasFileXattrs("foo/bar.txt", "foo", "bar"),
hasFileXattrs("foo/bar.txt", "invalid-utf8", invalidUtf8),
),
},
{
name: "2meta_2file",
in: tarOf(
dir("bar/", sampleOwner),
dir("foo/", sampleOwner),
file("foo/bar.txt", content, sampleOwner),
),
wantNumGz: 4, // both dirs, foo.txt alone, TOC, footer
want: checks(
numTOCEntries(3),
hasDir("bar/"),
hasDir("foo/"),
hasFileLen("foo/bar.txt", len(content)),
entryHasChildren("", "bar", "foo"),
entryHasChildren("foo", "bar.txt"),
hasChunkEntries("foo/bar.txt", 1),
hasEntryOwner("bar/", sampleOwner),
hasEntryOwner("foo/", sampleOwner),
hasEntryOwner("foo/bar.txt", sampleOwner),
),
},
{
name: "3dir",
in: tarOf(
dir("bar/"),
dir("foo/"),
dir("foo/bar/"),
),
wantNumGz: 3, // 3 dirs, TOC, footer
want: checks(
hasDirLinkCount("bar/", 2),
hasDirLinkCount("foo/", 3),
hasDirLinkCount("foo/bar/", 2),
),
},
{
name: "symlink",
in: tarOf(
dir("foo/"),
symlink("foo/bar", "../../x"),
),
wantNumGz: 3, // metas + TOC + footer
want: checks(
numTOCEntries(2),
hasSymlink("foo/bar", "../../x"),
entryHasChildren("", "foo"),
entryHasChildren("foo", "bar"),
),
},
{
name: "chunked_file",
chunkSize: 4,
in: tarOf(
dir("foo/"),
file("foo/big.txt", "This "+"is s"+"uch "+"a bi"+"g fi"+"le"),
),
wantNumGz: 9, // dir + big.txt(6 chunks) + TOC + footer
want: checks(
numTOCEntries(7), // 1 for foo dir, 6 for the foo/big.txt file
hasDir("foo/"),
hasFileLen("foo/big.txt", len("This is such a big file")),
hasFileDigest("foo/big.txt", digestFor("This is such a big file")),
hasFileContentsRange("foo/big.txt", 0, "This is such a big file"),
hasFileContentsRange("foo/big.txt", 1, "his is such a big file"),
hasFileContentsRange("foo/big.txt", 2, "is is such a big file"),
hasFileContentsRange("foo/big.txt", 3, "s is such a big file"),
hasFileContentsRange("foo/big.txt", 4, " is such a big file"),
hasFileContentsRange("foo/big.txt", 5, "is such a big file"),
hasFileContentsRange("foo/big.txt", 6, "s such a big file"),
hasFileContentsRange("foo/big.txt", 7, " such a big file"),
hasFileContentsRange("foo/big.txt", 8, "such a big file"),
hasFileContentsRange("foo/big.txt", 9, "uch a big file"),
hasFileContentsRange("foo/big.txt", 10, "ch a big file"),
hasFileContentsRange("foo/big.txt", 11, "h a big file"),
hasFileContentsRange("foo/big.txt", 12, " a big file"),
hasFileContentsRange("foo/big.txt", len("This is such a big file")-1, ""),
hasChunkEntries("foo/big.txt", 6),
),
},
{
name: "recursive",
in: tarOf(
dir("/", sampleOwner),
dir("bar/", sampleOwner),
dir("foo/", sampleOwner),
file("foo/bar.txt", content, sampleOwner),
),
wantNumGz: 4, // dirs, bar.txt alone, TOC, footer
want: checks(
maxDepth(2), // 0: root directory, 1: "foo/", 2: "bar.txt"
),
},
{
name: "block_char_fifo",
in: tarOf(
tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Name: prefix + "b",
Typeflag: tar.TypeBlock,
Devmajor: 123,
Devminor: 456,
Format: format,
})
}),
tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Name: prefix + "c",
Typeflag: tar.TypeChar,
Devmajor: 111,
Devminor: 222,
Format: format,
})
}),
tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Name: prefix + "f",
Typeflag: tar.TypeFifo,
Format: format,
})
}),
),
wantNumGz: 3,
want: checks(
lookupMatch("b", &TOCEntry{Name: "b", Type: "block", DevMajor: 123, DevMinor: 456, NumLink: 1}),
lookupMatch("c", &TOCEntry{Name: "c", Type: "char", DevMajor: 111, DevMinor: 222, NumLink: 1}),
lookupMatch("f", &TOCEntry{Name: "f", Type: "fifo", NumLink: 1}),
),
},
{
name: "modes",
in: tarOf(
dir("foo1/", 0755|os.ModeDir|os.ModeSetgid),
file("foo1/bar1", content, 0700|os.ModeSetuid),
file("foo1/bar2", content, 0755|os.ModeSetgid),
dir("foo2/", 0755|os.ModeDir|os.ModeSticky),
file("foo2/bar3", content, 0755|os.ModeSticky),
dir("foo3/", 0755|os.ModeDir),
file("foo3/bar4", content, os.FileMode(0700)),
file("foo3/bar5", content, os.FileMode(0755)),
),
wantNumGz: 8, // dir, bar1 alone, bar2 alone + dir, bar3 alone + dir, bar4 alone, bar5 alone, TOC, footer
want: checks(
hasMode("foo1/", 0755|os.ModeDir|os.ModeSetgid),
hasMode("foo1/bar1", 0700|os.ModeSetuid),
hasMode("foo1/bar2", 0755|os.ModeSetgid),
hasMode("foo2/", 0755|os.ModeDir|os.ModeSticky),
hasMode("foo2/bar3", 0755|os.ModeSticky),
hasMode("foo3/", 0755|os.ModeDir),
hasMode("foo3/bar4", os.FileMode(0700)),
hasMode("foo3/bar5", os.FileMode(0755)),
),
},
{
name: "lossy",
in: tarOf(
dir("bar/", sampleOwner),
dir("foo/", sampleOwner),
file("foo/bar.txt", content, sampleOwner),
file(TOCTarName, "dummy"), // ignored by the writer. (lossless write returns error)
),
wantNumGz: 4, // both dirs, foo.txt alone, TOC, footer
want: checks(
numTOCEntries(3),
hasDir("bar/"),
hasDir("foo/"),
hasFileLen("foo/bar.txt", len(content)),
entryHasChildren("", "bar", "foo"),
entryHasChildren("foo", "bar.txt"),
hasChunkEntries("foo/bar.txt", 1),
hasEntryOwner("bar/", sampleOwner),
hasEntryOwner("foo/", sampleOwner),
hasEntryOwner("foo/bar.txt", sampleOwner),
),
wantFailOnLossLess: true,
},
{
name: "hardlink should be replaced to the destination entry",
in: tarOf(
dir("foo/"),
file("foo/foo1", "test"),
link("foolink", "foo/foo1"),
),
wantNumGz: 4, // dir, foo1 + link, TOC, footer
want: checks(
mustSameEntry("foo/foo1", "foolink"),
),
},
{
name: "several_files_in_chunk",
minChunkSize: 8000,
in: tarOf(
dir("foo/"),
file("foo/foo1", data64KB),
file("foo2", "bb"),
file("foo22", "ccc"),
dir("bar/"),
file("bar/bar.txt", "aaa"),
file("foo3", data64KB),
),
// NOTE: we assume that the compressed "data64KB" is still larger than 8KB
wantNumGz: 4, // dir+foo1, foo2+foo22+dir+bar.txt+foo3, TOC, footer
want: checks(
numTOCEntries(7), // dir, foo1, foo2, foo22, dir, bar.txt, foo3
hasDir("foo/"),
hasDir("bar/"),
hasFileLen("foo/foo1", len(data64KB)),
hasFileLen("foo2", len("bb")),
hasFileLen("foo22", len("ccc")),
hasFileLen("bar/bar.txt", len("aaa")),
hasFileLen("foo3", len(data64KB)),
hasFileDigest("foo/foo1", digestFor(data64KB)),
hasFileDigest("foo2", digestFor("bb")),
hasFileDigest("foo22", digestFor("ccc")),
hasFileDigest("bar/bar.txt", digestFor("aaa")),
hasFileDigest("foo3", digestFor(data64KB)),
hasFileContentsWithPreRead("foo22", 0, "ccc", chunkInfo{"foo2", "bb"}, chunkInfo{"bar/bar.txt", "aaa"}, chunkInfo{"foo3", data64KB}),
hasFileContentsRange("foo/foo1", 0, data64KB),
hasFileContentsRange("foo2", 0, "bb"),
hasFileContentsRange("foo2", 1, "b"),
hasFileContentsRange("foo22", 0, "ccc"),
hasFileContentsRange("foo22", 1, "cc"),
hasFileContentsRange("foo22", 2, "c"),
hasFileContentsRange("bar/bar.txt", 0, "aaa"),
hasFileContentsRange("bar/bar.txt", 1, "aa"),
hasFileContentsRange("bar/bar.txt", 2, "a"),
hasFileContentsRange("foo3", 0, data64KB),
hasFileContentsRange("foo3", 1, data64KB[1:]),
hasFileContentsRange("foo3", 2, data64KB[2:]),
hasFileContentsRange("foo3", len(data64KB)/2, data64KB[len(data64KB)/2:]),
hasFileContentsRange("foo3", len(data64KB)-1, data64KB[len(data64KB)-1:]),
),
},
{
name: "several_files_in_chunk_chunked",
minChunkSize: 8000,
chunkSize: 32000,
in: tarOf(
dir("foo/"),
file("foo/foo1", data64KB),
file("foo2", "bb"),
dir("bar/"),
file("foo3", data64KB),
),
// NOTE: we assume that the compressed chunk of "data64KB" is still larger than 8KB
wantNumGz: 6, // dir+foo1(1), foo1(2), foo2+dir+foo3(1), foo3(2), TOC, footer
want: checks(
numTOCEntries(7), // dir, foo1(2 chunks), foo2, dir, foo3(2 chunks)
hasDir("foo/"),
hasDir("bar/"),
hasFileLen("foo/foo1", len(data64KB)),
hasFileLen("foo2", len("bb")),
hasFileLen("foo3", len(data64KB)),
hasFileDigest("foo/foo1", digestFor(data64KB)),
hasFileDigest("foo2", digestFor("bb")),
hasFileDigest("foo3", digestFor(data64KB)),
hasFileContentsWithPreRead("foo2", 0, "bb", chunkInfo{"foo3", data64KB[:32000]}),
hasFileContentsRange("foo/foo1", 0, data64KB),
hasFileContentsRange("foo/foo1", 1, data64KB[1:]),
hasFileContentsRange("foo/foo1", 2, data64KB[2:]),
hasFileContentsRange("foo/foo1", len(data64KB)/2, data64KB[len(data64KB)/2:]),
hasFileContentsRange("foo/foo1", len(data64KB)-1, data64KB[len(data64KB)-1:]),
hasFileContentsRange("foo2", 0, "bb"),
hasFileContentsRange("foo2", 1, "b"),
hasFileContentsRange("foo3", 0, data64KB),
hasFileContentsRange("foo3", 1, data64KB[1:]),
hasFileContentsRange("foo3", 2, data64KB[2:]),
hasFileContentsRange("foo3", len(data64KB)/2, data64KB[len(data64KB)/2:]),
hasFileContentsRange("foo3", len(data64KB)-1, data64KB[len(data64KB)-1:]),
),
},
}
for _, tt := range tests {
for _, newCL := range controllers {
newCL := newCL
for _, prefix := range allowedPrefix {
prefix := prefix
for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} {
srcTarFormat := srcTarFormat
for _, lossless := range []bool{true, false} {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,lossless=%v,format=%s", newCL(), prefix, lossless, srcTarFormat), func(t *TestRunner) {
var tr io.Reader = buildTar(t, tt.in, prefix, srcTarFormat)
origTarDgstr := digest.Canonical.Digester()
tr = io.TeeReader(tr, origTarDgstr.Hash())
var stargzBuf bytes.Buffer
cl1 := newCL()
w := NewWriterWithCompressor(&stargzBuf, cl1)
w.ChunkSize = tt.chunkSize
w.MinChunkSize = tt.minChunkSize
if lossless {
err := w.AppendTarLossLess(tr)
if tt.wantFailOnLossLess {
if err != nil {
return // expected to fail
}
t.Fatalf("Append wanted to fail on lossless")
}
if err != nil {
t.Fatalf("Append(lossless): %v", err)
}
} else {
if err := w.AppendTar(tr); err != nil {
t.Fatalf("Append: %v", err)
}
}
if _, err := w.Close(); err != nil {
t.Fatalf("Writer.Close: %v", err)
}
b := stargzBuf.Bytes()
if lossless {
// Check if the result blob reserves original tar metadata
rc, err := Unpack(io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))), cl1)
if err != nil {
t.Errorf("failed to decompress blob: %v", err)
return
}
defer rc.Close()
resultDgstr := digest.Canonical.Digester()
if _, err := io.Copy(resultDgstr.Hash(), rc); err != nil {
t.Errorf("failed to read result decompressed blob: %v", err)
return
}
if resultDgstr.Digest() != origTarDgstr.Digest() {
t.Errorf("lossy compression occurred: digest=%v; want %v",
resultDgstr.Digest(), origTarDgstr.Digest())
return
}
}
diffID := w.DiffID()
wantDiffID := cl1.DiffIDOf(t, b)
if diffID != wantDiffID {
t.Errorf("DiffID = %q; want %q", diffID, wantDiffID)
}
telemetry, checkCalled := newCalledTelemetry()
sr := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b)))
r, err := Open(
sr,
WithDecompressors(cl1),
WithTelemetry(telemetry),
)
if err != nil {
t.Fatalf("stargz.Open: %v", err)
}
if _, ok := r.Lookup(""); !ok {
t.Fatalf("failed to lookup rootdir: %v", err)
}
wantTOCVersion := 1
if tt.wantTOCVersion > 0 {
wantTOCVersion = tt.wantTOCVersion
}
if r.toc.Version != wantTOCVersion {
t.Fatalf("invalid TOC Version %d; wanted %d", r.toc.Version, wantTOCVersion)
}
footerSize := cl1.FooterSize()
footerOffset := sr.Size() - footerSize
footer := make([]byte, footerSize)
if _, err := sr.ReadAt(footer, footerOffset); err != nil {
t.Errorf("failed to read footer: %v", err)
}
_, tocOffset, _, err := cl1.ParseFooter(footer)
if err != nil {
t.Errorf("failed to parse footer: %v", err)
}
if err := checkCalled(tocOffset >= 0); err != nil {
t.Errorf("telemetry failure: %v", err)
}
wantNumGz := tt.wantNumGz
if lossless && tt.wantNumGzLossLess > 0 {
wantNumGz = tt.wantNumGzLossLess
}
streamOffsets := []int64{0}
prevOffset := int64(-1)
streams := 0
for _, e := range r.toc.Entries {
if e.Offset > prevOffset {
streamOffsets = append(streamOffsets, e.Offset)
prevOffset = e.Offset
streams++
}
}
streams++ // TOC
if tocOffset >= 0 {
// toc is in the blob
streamOffsets = append(streamOffsets, tocOffset)
}
streams++ // footer
streamOffsets = append(streamOffsets, footerOffset)
if streams != wantNumGz {
t.Errorf("number of streams in TOC = %d; want %d", streams, wantNumGz)
}
t.Logf("testing streams: %+v", streamOffsets)
cl1.TestStreams(t, b, streamOffsets)
for _, want := range tt.want {
want.check(t, r)
}
})
}
}
}
}
}
}
type chunkInfo struct {
name string
data string
}
func newCalledTelemetry() (telemetry *Telemetry, check func(needsGetTOC bool) error) {
var getFooterLatencyCalled bool
var getTocLatencyCalled bool
var deserializeTocLatencyCalled bool
return &Telemetry{
func(time.Time) { getFooterLatencyCalled = true },
func(time.Time) { getTocLatencyCalled = true },
func(time.Time) { deserializeTocLatencyCalled = true },
}, func(needsGetTOC bool) error {
var allErr []error
if !getFooterLatencyCalled {
allErr = append(allErr, fmt.Errorf("metrics GetFooterLatency isn't called"))
}
if needsGetTOC {
if !getTocLatencyCalled {
allErr = append(allErr, fmt.Errorf("metrics GetTocLatency isn't called"))
}
}
if !deserializeTocLatencyCalled {
allErr = append(allErr, fmt.Errorf("metrics DeserializeTocLatency isn't called"))
}
return errorutil.Aggregate(allErr)
}
}
func digestFor(content string) string {
sum := sha256.Sum256([]byte(content))
return fmt.Sprintf("sha256:%x", sum)
}
type numTOCEntries int
func (n numTOCEntries) check(t TestingT, r *Reader) {
if r.toc == nil {
t.Fatal("nil TOC")
}
if got, want := len(r.toc.Entries), int(n); got != want {
t.Errorf("got %d TOC entries; want %d", got, want)
}
t.Logf("got TOC entries:")
for i, ent := range r.toc.Entries {
entj, _ := json.Marshal(ent)
t.Logf(" [%d]: %s\n", i, entj)
}
if t.Failed() {
t.FailNow()
}
}
func checks(s ...stargzCheck) []stargzCheck { return s }
type stargzCheck interface {
check(t TestingT, r *Reader)
}
type stargzCheckFn func(TestingT, *Reader)
func (f stargzCheckFn) check(t TestingT, r *Reader) { f(t, r) }
func maxDepth(max int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
e, ok := r.Lookup("")
if !ok {
t.Fatal("root directory not found")
}
d, err := getMaxDepth(t, e, 0, 10*max)
if err != nil {
t.Errorf("failed to get max depth (wanted %d): %v", max, err)
return
}
if d != max {
t.Errorf("invalid depth %d; want %d", d, max)
return
}
})
}
func getMaxDepth(t TestingT, e *TOCEntry, current, limit int) (max int, rErr error) {
if current > limit {
return -1, fmt.Errorf("walkMaxDepth: exceeds limit: current:%d > limit:%d",
current, limit)
}
max = current
e.ForeachChild(func(baseName string, ent *TOCEntry) bool {
t.Logf("%q(basename:%q) is child of %q\n", ent.Name, baseName, e.Name)
d, err := getMaxDepth(t, ent, current+1, limit)
if err != nil {
rErr = err
return false
}
if d > max {
max = d
}
return true
})
return
}
func hasFileLen(file string, wantLen int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
t.Errorf("file type of %q is %q; want \"reg\"", file, ent.Type)
} else if ent.Size != int64(wantLen) {
t.Errorf("file size of %q = %d; want %d", file, ent.Size, wantLen)
}
return
}
}
t.Errorf("file %q not found", file)
})
}
func hasFileXattrs(file, name, value string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
t.Errorf("file type of %q is %q; want \"reg\"", file, ent.Type)
}
if ent.Xattrs == nil {
t.Errorf("file %q has no xattrs", file)
return
}
valueFound, found := ent.Xattrs[name]
if !found {
t.Errorf("file %q has no xattr %q", file, name)
return
}
if string(valueFound) != value {
t.Errorf("file %q has xattr %q with value %q instead of %q", file, name, valueFound, value)
}
return
}
}
t.Errorf("file %q not found", file)
})
}
func hasFileDigest(file string, digest string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("didn't find TOCEntry for file %q", file)
}
if ent.Digest != digest {
t.Fatalf("Digest(%q) = %q, want %q", file, ent.Digest, digest)
}
})
}
func hasFileContentsWithPreRead(file string, offset int, want string, extra ...chunkInfo) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
extraMap := make(map[string]chunkInfo)
for _, e := range extra {
extraMap[e.name] = e
}
var extraNames []string
for n := range extraMap {
extraNames = append(extraNames, n)
}
f, err := r.OpenFileWithPreReader(file, func(e *TOCEntry, cr io.Reader) error {
t.Logf("On %q: got preread of %q", file, e.Name)
ex, ok := extraMap[e.Name]
if !ok {
t.Fatalf("fail on %q: unexpected entry %q: %+v, %+v", file, e.Name, e, extraNames)
}
got, err := io.ReadAll(cr)
if err != nil {
t.Fatalf("fail on %q: failed to read %q: %v", file, e.Name, err)
}
if ex.data != string(got) {
t.Fatalf("fail on %q: unexpected contents of %q: len=%d; want=%d", file, e.Name, len(got), len(ex.data))
}
delete(extraMap, e.Name)
return nil
})
if err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
n, err := f.ReadAt(got, int64(offset))
if err != nil {
t.Fatalf("ReadAt(len %d, offset %d, size %d) = %v, %v", len(got), offset, f.Size(), n, err)
}
if string(got) != want {
t.Fatalf("ReadAt(len %d, offset %d) = %q, want %q", len(got), offset, viewContent(got), viewContent([]byte(want)))
}
if len(extraMap) != 0 {
var exNames []string
for _, ex := range extraMap {
exNames = append(exNames, ex.name)
}
t.Fatalf("fail on %q: some entries aren't read: %+v", file, exNames)
}
})
}
func hasFileContentsRange(file string, offset int, want string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
f, err := r.OpenFile(file)
if err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
n, err := f.ReadAt(got, int64(offset))
if err != nil {
t.Fatalf("ReadAt(len %d, offset %d) = %v, %v", len(got), offset, n, err)
}
if string(got) != want {
t.Fatalf("ReadAt(len %d, offset %d) = %q, want %q", len(got), offset, viewContent(got), viewContent([]byte(want)))
}
})
}
func hasChunkEntries(file string, wantChunks int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("no file for %q", file)
}
if ent.Type != "reg" {
t.Fatalf("file %q has unexpected type %q; want reg", file, ent.Type)
}
chunks := r.getChunks(ent)
if len(chunks) != wantChunks {
t.Errorf("len(r.getChunks(%q)) = %d; want %d", file, len(chunks), wantChunks)
return
}
f := chunks[0]
var gotChunks []*TOCEntry
var last *TOCEntry
for off := int64(0); off < f.Size; off++ {
e, ok := r.ChunkEntryForOffset(file, off)
if !ok {
t.Errorf("no ChunkEntryForOffset at %d", off)
return
}
if last != e {
gotChunks = append(gotChunks, e)
last = e
}
}
if !reflect.DeepEqual(chunks, gotChunks) {
t.Errorf("gotChunks=%d, want=%d; contents mismatch", len(gotChunks), wantChunks)
}
// And verify the NextOffset
for i := 0; i < len(gotChunks)-1; i++ {
ci := gotChunks[i]
cnext := gotChunks[i+1]
if ci.NextOffset() != cnext.Offset {
t.Errorf("chunk %d NextOffset %d != next chunk's Offset of %d", i, ci.NextOffset(), cnext.Offset)
}
}
})
}
func entryHasChildren(dir string, want ...string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
want := append([]string(nil), want...)
var got []string
ent, ok := r.Lookup(dir)
if !ok {
t.Fatalf("didn't find TOCEntry for dir node %q", dir)
}
for baseName := range ent.children {
got = append(got, baseName)
}
sort.Strings(got)
sort.Strings(want)
if !reflect.DeepEqual(got, want) {
t.Errorf("children of %q = %q; want %q", dir, got, want)
}
})
}
func hasDir(file string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
t.Errorf("file type of %q is %q; want \"dir\"", file, ent.Type)
}
return
}
}
t.Errorf("directory %q not found", file)
})
}
func hasDirLinkCount(file string, count int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
t.Errorf("file type of %q is %q; want \"dir\"", file, ent.Type)
return
}
if ent.NumLink != count {
t.Errorf("link count of %q = %d; want %d", file, ent.NumLink, count)
}
return
}
}
t.Errorf("directory %q not found", file)
})
}
func hasMode(file string, mode os.FileMode) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Stat().Mode() != mode {
t.Errorf("invalid mode: got %v; want %v", ent.Stat().Mode(), mode)
return
}
return
}
}
t.Errorf("file %q not found", file)
})
}
func hasSymlink(file, target string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "symlink" {
t.Errorf("file type of %q is %q; want \"symlink\"", file, ent.Type)
} else if ent.LinkName != target {
t.Errorf("link target of symlink %q is %q; want %q", file, ent.LinkName, target)
}
return
}
}
t.Errorf("symlink %q not found", file)
})
}
func lookupMatch(name string, want *TOCEntry) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
e, ok := r.Lookup(name)
if !ok {
t.Fatalf("failed to Lookup entry %q", name)
}
if !reflect.DeepEqual(e, want) {
t.Errorf("entry %q mismatch.\n got: %+v\nwant: %+v\n", name, e, want)
}
})
}
func hasEntryOwner(entry string, owner owner) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(strings.TrimSuffix(entry, "/"))
if !ok {
t.Errorf("entry %q not found", entry)
return
}
if ent.UID != owner.uid || ent.GID != owner.gid {
t.Errorf("entry %q has invalid owner (uid:%d, gid:%d) instead of (uid:%d, gid:%d)", entry, ent.UID, ent.GID, owner.uid, owner.gid)
return
}
})
}
func mustSameEntry(files ...string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
var first *TOCEntry
for _, f := range files {
if first == nil {
var ok bool
first, ok = r.Lookup(f)
if !ok {
t.Errorf("unknown first file on Lookup: %q", f)
return
}
}
// Test Lookup
e, ok := r.Lookup(f)
if !ok {
t.Errorf("unknown file on Lookup: %q", f)
return
}
if e != first {
t.Errorf("Lookup: %+v(%p) != %+v(%p)", e, e, first, first)
return
}
// Test LookupChild
pe, ok := r.Lookup(filepath.Dir(filepath.Clean(f)))
if !ok {
t.Errorf("failed to get parent of %q", f)
return
}
e, ok = pe.LookupChild(filepath.Base(filepath.Clean(f)))
if !ok {
t.Errorf("failed to get %q as the child of %+v", f, pe)
return
}
if e != first {
t.Errorf("LookupChild: %+v(%p) != %+v(%p)", e, e, first, first)
return
}
// Test ForeachChild
pe.ForeachChild(func(baseName string, e *TOCEntry) bool {
if baseName == filepath.Base(filepath.Clean(f)) {
if e != first {
t.Errorf("ForeachChild: %+v(%p) != %+v(%p)", e, e, first, first)
return false
}
}
return true
})
}
})
}
func viewContent(c []byte) string {
if len(c) < 100 {
return string(c)
}
return string(c[:50]) + "...(omit)..." + string(c[50:100])
}
func tarOf(s ...tarEntry) []tarEntry { return s }
type tarEntry interface {
appendTar(tw *tar.Writer, prefix string, format tar.Format) error
}
type tarEntryFunc func(*tar.Writer, string, tar.Format) error
func (f tarEntryFunc) appendTar(tw *tar.Writer, prefix string, format tar.Format) error {
return f(tw, prefix, format)
}
func buildTar(t TestingT, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader {
format := tar.FormatUnknown
for _, opt := range opts {
switch v := opt.(type) {
case tar.Format:
format = v
default:
panic(fmt.Errorf("unsupported opt for buildTar: %v", opt))
}
}
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
for _, ent := range ents {
if err := ent.appendTar(tw, prefix, format); err != nil {
t.Fatalf("building input tar: %v", err)
}
}
if err := tw.Close(); err != nil {
t.Errorf("closing write of input tar: %v", err)
}
data := append(buf.Bytes(), make([]byte, 100)...) // append empty bytes at the tail to see lossless works
return io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data)))
}
func dir(name string, opts ...interface{}) tarEntry {
return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error {
var o owner
mode := os.FileMode(0755)
for _, opt := range opts {
switch v := opt.(type) {
case owner:
o = v
case os.FileMode:
mode = v
default:
return errors.New("unsupported opt")
}
}
if !strings.HasSuffix(name, "/") {
panic(fmt.Sprintf("missing trailing slash in dir %q ", name))
}
tm, err := fileModeToTarMode(mode)
if err != nil {
return err
}
return tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeDir,
Name: prefix + name,
Mode: tm,
Uid: o.uid,
Gid: o.gid,
Format: format,
})
})
}
// xAttr are extended attributes to set on test files created with the file func.
type xAttr map[string]string
// owner is owner ot set on test files and directories with the file and dir functions.
type owner struct {
uid int
gid int
}
func file(name, contents string, opts ...interface{}) tarEntry {
return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error {
var xattrs xAttr
var o owner
mode := os.FileMode(0644)
for _, opt := range opts {
switch v := opt.(type) {
case xAttr:
xattrs = v
case owner:
o = v
case os.FileMode:
mode = v
default:
return errors.New("unsupported opt")
}
}
if strings.HasSuffix(name, "/") {
return fmt.Errorf("bogus trailing slash in file %q", name)
}
tm, err := fileModeToTarMode(mode)
if err != nil {
return err
}
if len(xattrs) > 0 {
format = tar.FormatPAX // only PAX supports xattrs
}
if err := tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: prefix + name,
Mode: tm,
Xattrs: xattrs,
Size: int64(len(contents)),
Uid: o.uid,
Gid: o.gid,
Format: format,
}); err != nil {
return err
}
_, err = io.WriteString(tw, contents)
return err
})
}
func symlink(name, target string) tarEntry {
return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error {
return tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeSymlink,
Name: prefix + name,
Linkname: target,
Mode: 0644,
Format: format,
})
})
}
func link(name string, linkname string) tarEntry {
now := time.Now()
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Typeflag: tar.TypeLink,
Name: prefix + name,
Linkname: linkname,
ModTime: now,
Format: format,
})
})
}
func chardev(name string, major, minor int64) tarEntry {
now := time.Now()
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Typeflag: tar.TypeChar,
Name: prefix + name,
Devmajor: major,
Devminor: minor,
ModTime: now,
Format: format,
})
})
}
func blockdev(name string, major, minor int64) tarEntry {
now := time.Now()
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Typeflag: tar.TypeBlock,
Name: prefix + name,
Devmajor: major,
Devminor: minor,
ModTime: now,
Format: format,
})
})
}
func fifo(name string) tarEntry {
now := time.Now()
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
return w.WriteHeader(&tar.Header{
Typeflag: tar.TypeFifo,
Name: prefix + name,
ModTime: now,
Format: format,
})
})
}
func prefetchLandmark() tarEntry {
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
if err := w.WriteHeader(&tar.Header{
Name: PrefetchLandmark,
Typeflag: tar.TypeReg,
Size: int64(len([]byte{landmarkContents})),
Format: format,
}); err != nil {
return err
}
contents := []byte{landmarkContents}
if _, err := io.CopyN(w, bytes.NewReader(contents), int64(len(contents))); err != nil {
return err
}
return nil
})
}
func noPrefetchLandmark() tarEntry {
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
if err := w.WriteHeader(&tar.Header{
Name: NoPrefetchLandmark,
Typeflag: tar.TypeReg,
Size: int64(len([]byte{landmarkContents})),
Format: format,
}); err != nil {
return err
}
contents := []byte{landmarkContents}
if _, err := io.CopyN(w, bytes.NewReader(contents), int64(len(contents))); err != nil {
return err
}
return nil
})
}
func regDigest(t TestingT, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry {
if digestMap == nil {
t.Fatalf("digest map mustn't be nil")
}
content := []byte(contentStr)
var n int64
for n < int64(len(content)) {
size := int64(chunkSize)
remain := int64(len(content)) - n
if remain < size {
size = remain
}
dgstr := digest.Canonical.Digester()
if _, err := io.CopyN(dgstr.Hash(), bytes.NewReader(content[n:n+size]), size); err != nil {
t.Fatalf("failed to calculate digest of %q (name=%q,offset=%d,size=%d)",
string(content[n:n+size]), name, n, size)
}
digestMap[chunkID(name, n, size)] = dgstr.Digest()
n += size
}
return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error {
if err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: prefix + name,
Size: int64(len(content)),
Format: format,
}); err != nil {
return err
}
if _, err := io.CopyN(w, bytes.NewReader(content), int64(len(content))); err != nil {
return err
}
return nil
})
}
var runes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randomContents(n int) string {
b := make([]rune, n)
for i := range b {
bi, err := rand.Int(rand.Reader, big.NewInt(int64(len(runes))))
if err != nil {
panic(err)
}
b[i] = runes[int(bi.Int64())]
}
return string(b)
}
func fileModeToTarMode(mode os.FileMode) (int64, error) {
h, err := tar.FileInfoHeader(fileInfoOnlyMode(mode), "")
if err != nil {
return 0, err
}
return h.Mode, nil
}
// fileInfoOnlyMode is os.FileMode that populates only file mode.
type fileInfoOnlyMode os.FileMode
func (f fileInfoOnlyMode) Name() string { return "" }
func (f fileInfoOnlyMode) Size() int64 { return 0 }
func (f fileInfoOnlyMode) Mode() os.FileMode { return os.FileMode(f) }
func (f fileInfoOnlyMode) ModTime() time.Time { return time.Now() }
func (f fileInfoOnlyMode) IsDir() bool { return os.FileMode(f).IsDir() }
func (f fileInfoOnlyMode) Sys() interface{} { return nil }
func CheckGzipHasStreams(t TestingT, b []byte, streams []int64) {
if len(streams) == 0 {
return // nop
}
wants := map[int64]struct{}{}
for _, s := range streams {
wants[s] = struct{}{}
}
len0 := len(b)
br := bytes.NewReader(b)
zr := new(gzip.Reader)
t.Logf("got gzip streams:")
numStreams := 0
for {
zoff := len0 - br.Len()
if err := zr.Reset(br); err != nil {
if err == io.EOF {
return
}
t.Fatalf("countStreams(gzip), Reset: %v", err)
}
zr.Multistream(false)
n, err := io.Copy(io.Discard, zr)
if err != nil {
t.Fatalf("countStreams(gzip), Copy: %v", err)
}
var extra string
if len(zr.Extra) > 0 {
extra = fmt.Sprintf("; extra=%q", zr.Extra)
}
t.Logf(" [%d] at %d in stargz, uncompressed length %d%s", numStreams, zoff, n, extra)
delete(wants, int64(zoff))
numStreams++
}
}
func GzipDiffIDOf(t TestingT, b []byte) string {
h := sha256.New()
zr, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
t.Fatalf("diffIDOf(gzip): %v", err)
}
defer zr.Close()
if _, err := io.Copy(h, zr); err != nil {
t.Fatalf("diffIDOf(gzip).Copy: %v", err)
}
return fmt.Sprintf("sha256:%x", h.Sum(nil))
}
================================================
FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/types.go
================================================
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
package estargz
import (
"archive/tar"
"hash"
"io"
"os"
"path"
"time"
digest "github.com/opencontainers/go-digest"
)
const (
// TOCTarName is the name of the JSON file in the tar archive in the
// table of contents gzip stream.
TOCTarName = "stargz.index.json"
// FooterSize is the number of bytes in the footer
//
// The footer is an empty gzip stream with no compression and an Extra
// header of the form "%016xSTARGZ", where the 64 bit hex-encoded
// number is the offset to the gzip stream of JSON TOC.
//
// 51 comes from:
//
// 10 bytes gzip header
// 2 bytes XLEN (length of Extra field) = 26 (4 bytes header + 16 hex digits + len("STARGZ"))
// 2 bytes Extra: SI1 = 'S', SI2 = 'G'
// 2 bytes Extra: LEN = 22 (16 hex digits + len("STARGZ"))
// 22 bytes Extra: subfield = fmt.Sprintf("%016xSTARGZ", offsetOfTOC)
// 5 bytes flate header
// 8 bytes gzip footer
// (End of the eStargz blob)
//
// NOTE: For Extra fields, subfield IDs SI1='S' SI2='G' is used for eStargz.
FooterSize = 51
// legacyFooterSize is the number of bytes in the legacy stargz footer.
//
// 47 comes from:
//
// 10 byte gzip header +
// 2 byte (LE16) length of extra, encoding 22 (16 hex digits + len("STARGZ")) == "\x16\x00" +
// 22 bytes of extra (fmt.Sprintf("%016xSTARGZ", tocGzipOffset))
// 5 byte flate header
// 8 byte gzip footer (two little endian uint32s: digest, size)
legacyFooterSize = 47
// TOCJSONDigestAnnotation is an annotation for an image layer. This stores the
// digest of the TOC JSON.
// This annotation is valid only when it is specified in `.[]layers.annotations`
// of an image manifest.
TOCJSONDigestAnnotation = "containerd.io/snapshot/stargz/toc.digest"
// StoreUncompressedSizeAnnotation is an additional annotation key for eStargz to enable lazy
// pulling on containers/storage. Stargz Store is required to expose the layer's uncompressed size
// to the runtime but current OCI image doesn't ship this information by default. So we store this
// to the special annotation.
StoreUncompressedSizeAnnotation = "io.containers.estargz.uncompressed-size"
// PrefetchLandmark is a file entry which indicates the end position of
// prefetch in the stargz file.
PrefetchLandmark = ".prefetch.landmark"
// NoPrefetchLandmark is a file entry which indicates that no prefetch should
// occur in the stargz file.
NoPrefetchLandmark = ".no.prefetch.landmark"
landmarkContents = 0xf
)
// JTOC is the JSON-serialized table of contents index of the files in the stargz file.
type JTOC struct {
Version int `json:"version"`
Entries []*TOCEntry `json:"entries"`
}
// TOCEntry is an entry in the stargz file's TOC (Table of Contents).
type TOCEntry struct {
// Name is the tar entry's name. It is the complete path
// stored in the tar file, not just the base name.
Name string `json:"name"`
// Type is one of "dir", "reg", "symlink", "hardlink", "char",
// "block", "fifo", or "chunk".
// The "chunk" type is used for regular file data chunks past the first
// TOCEntry; the 2nd chunk and on have only Type ("chunk"), Offset,
// ChunkOffset, and ChunkSize populated.
Type string `json:"type"`
// Size, for regular files, is the logical size of the file.
Size int64 `json:"size,omitempty"`
// ModTime3339 is the modification time of the tar entry. Empty
// means zero or unknown. Otherwise it's in UTC RFC3339
// format. Use the ModTime method to access the time.Time value.
ModTime3339 string `json:"modtime,omitempty"`
modTime time.Time
// LinkName, for symlinks and hardlinks, is the link target.
LinkName string `json:"linkName,omitempty"`
// Mode is the permission and mode bits.
Mode int64 `json:"mode,omitempty"`
// UID is the user ID of the owner.
UID int `json:"uid,omitempty"`
// GID is the group ID of the owner.
GID int `json:"gid,omitempty"`
// Uname is the username of the owner.
//
// In the serialized JSON, this field may only be present for
// the first entry with the same UID.
Uname string `json:"userName,omitempty"`
// Gname is the group name of the owner.
//
// In the serialized JSON, this field may only be present for
// the first entry with the same GID.
Gname string `json:"groupName,omitempty"`
// Offset, for regular files, provides the offset in the
// stargz file to the file's data bytes. See ChunkOffset and
// ChunkSize.
Offset int64 `json:"offset,omitempty"`
// InnerOffset is an optional field indicates uncompressed offset
// of this "reg" or "chunk" payload in a stream starts from Offset.
// This field enables to put multiple "reg" or "chunk" payloads
// in one chunk with having the same Offset but different InnerOffset.
InnerOffset int64 `json:"innerOffset,omitempty"`
nextOffset int64 // the Offset of the next entry with a non-zero Offset
// DevMajor is the major device number for "char" and "block" types.
DevMajor int `json:"devMajor,omitempty"`
// DevMinor is the major device number for "char" and "block" types.
DevMinor int `json:"devMinor,omitempty"`
// NumLink is the number of entry names pointing to this entry.
// Zero means one name references this entry.
// This field is calculated during runtime and not recorded in TOC JSON.
NumLink int `json:"-"`
// Xattrs are the extended attribute for the entry.
Xattrs map[string][]byte `json:"xattrs,omitempty"`
// Digest stores the OCI checksum for regular files payload.
// It has the form "sha256:abcdef01234....".
Digest string `json:"digest,omitempty"`
// ChunkOffset is non-zero if this is a chunk of a large,
// regular file. If so, the Offset is where the gzip header of
// ChunkSize bytes at ChunkOffset in Name begin.
//
// In serialized form, a "chunkSize" JSON field of zero means
// that the chunk goes to the end of the file. After reading
// from the stargz TOC, though, the ChunkSize is initialized
// to a non-zero file for when Type is either "reg" or
// "chunk".
ChunkOffset int64 `json:"chunkOffset,omitempty"`
ChunkSize int64 `json:"chunkSize,omitempty"`
// ChunkDigest stores an OCI digest of the chunk. This must be formed
// as "sha256:0123abcd...".
ChunkDigest string `json:"chunkDigest,omitempty"`
children map[string]*TOCEntry
// chunkTopIndex is index of the entry where Offset starts in the blob.
chunkTopIndex int
}
// ModTime returns the entry's modification time.
func (e *TOCEntry) ModTime() time.Time { return e.modTime }
// NextOffset returns the position (relative to the start of the
// stargz file) of the next gzip boundary after e.Offset.
func (e *TOCEntry) NextOffset() int64 { return e.nextOffset }
func (e *TOCEntry) addChild(baseName string, child *TOCEntry) {
if e.children == nil {
e.children = make(map[string]*TOCEntry)
}
if child.Type == "dir" {
e.NumLink++ // Entry ".." in the subdirectory links to this directory
}
e.children[baseName] = child
}
// isDataType reports whether TOCEntry is a regular file or chunk (something that
// contains regular file data).
func (e *TOCEntry) isDataType() bool { return e.Type == "reg" || e.Type == "chunk" }
// Stat returns a FileInfo value representing e.
func (e *TOCEntry) Stat() os.FileInfo { return fileInfo{e} }
// ForeachChild calls f for each child item. If f returns false, iteration ends.
// If e is not a directory, f is not called.
func (e *TOCEntry) ForeachChild(f func(baseName string, ent *TOCEntry) bool) {
for name, ent := range e.children {
if !f(name, ent) {
return
}
}
}
// LookupChild returns the directory e's child by its base name.
func (e *TOCEntry) LookupChild(baseName string) (child *TOCEntry, ok bool) {
child, ok = e.children[baseName]
return
}
// fileInfo implements os.FileInfo using the wrapped *TOCEntry.
type fileInfo struct{ e *TOCEntry }
var _ os.FileInfo = fileInfo{}
func (fi fileInfo) Name() string { return path.Base(fi.e.Name) }
func (fi fileInfo) IsDir() bool { return fi.e.Type == "dir" }
func (fi fileInfo) Size() int64 { return fi.e.Size }
func (fi fileInfo) ModTime() time.Time { return fi.e.ModTime() }
func (fi fileInfo) Sys() interface{} { return fi.e }
func (fi fileInfo) Mode() (m os.FileMode) {
// TOCEntry.Mode is tar.Header.Mode so we can understand the these bits using `tar` pkg.
m = (&tar.Header{Mode: fi.e.Mode}).FileInfo().Mode() &
(os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky)
switch fi.e.Type {
case "dir":
m |= os.ModeDir
case "symlink":
m |= os.ModeSymlink
case "char":
m |= os.ModeDevice | os.ModeCharDevice
case "block":
m |= os.ModeDevice
case "fifo":
m |= os.ModeNamedPipe
}
return m
}
// TOCEntryVerifier holds verifiers that are usable for verifying chunks contained
// in a eStargz blob.
type TOCEntryVerifier interface {
// Verifier provides a content verifier that can be used for verifying the
// contents of the specified TOCEntry.
Verifier(ce *TOCEntry) (digest.Verifier, error)
}
// Compression provides the compression helper to be used creating and parsing eStargz.
// This package provides gzip-based Compression by default, but any compression
// algorithm (e.g. zstd) can be used as long as it implements Compression.
type Compression interface {
Compressor
Decompressor
}
// Compressor represents the helper mothods to be used for creating eStargz.
type Compressor interface {
// Writer returns WriteCloser to be used for writing a chunk to eStargz.
// Everytime a chunk is written, the WriteCloser is closed and Writer is
// called again for writing the next chunk.
//
// The returned writer should implement "Flush() error" function that flushes
// any pending compressed data to the underlying writer.
Writer(w io.Writer) (WriteFlushCloser, error)
// WriteTOCAndFooter is called to write JTOC to the passed Writer.
// diffHash calculates the DiffID (uncompressed sha256 hash) of the blob
// WriteTOCAndFooter can optionally write anything that affects DiffID calculation
// (e.g. uncompressed TOC JSON).
//
// This function returns tocDgst that represents the digest of TOC that will be used
// to verify this blob when it's parsed.
WriteTOCAndFooter(w io.Writer, off int64, toc *JTOC, diffHash hash.Hash) (tocDgst digest.Digest, err error)
}
// Decompressor represents the helper mothods to be used for parsing eStargz.
type Decompressor interface {
// Reader returns ReadCloser to be used for decompressing file payload.
Reader(r io.Reader) (io.ReadCloser, error)
// FooterSize returns the size of the footer of this blob.
FooterSize() int64
// ParseFooter parses the footer and returns the offset and (compressed) size of TOC.
// payloadBlobSize is the (compressed) size of the blob payload (i.e. the size between
// the top until the TOC JSON).
//
// If tocOffset < 0, we assume that TOC isn't contained in the blob and pass nil reader
// to ParseTOC. We expect that ParseTOC acquire TOC from the external location and return it.
//
// tocSize is optional. If tocSize <= 0, it's by default the size of the range from tocOffset until the beginning of the
// footer (blob size - tocOff - FooterSize).
// If blobPayloadSize < 0, blobPayloadSize become the blob size.
ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error)
// ParseTOC parses TOC from the passed reader. The reader provides the partial contents
// of the underlying blob that has the range specified by ParseFooter method.
//
// This function returns tocDgst that represents the digest of TOC that will be used
// to verify this blob. This must match to the value returned from
// Compressor.WriteTOCAndFooter that is used when creating this blob.
//
// If tocOffset returned by ParseFooter is < 0, we assume that TOC isn't contained in the blob.
// Pass nil reader to ParseTOC then we expect that ParseTOC acquire TOC from the external location
// and return it.
ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error)
}
type WriteFlushCloser interface {
io.WriteCloser
Flush() error
}
================================================
FILE: vendor/github.com/docker/cli/AUTHORS
================================================
# File @generated by scripts/docs/generate-authors.sh. DO NOT EDIT.
# This file lists all contributors to the repository.
# See scripts/docs/generate-authors.sh to make modifications.
A. Lester Buck III
Aanand Prasad
Aaron L. Xu
Aaron Lehmann
Aaron.L.Xu
Abdur Rehman
Abhinandan Prativadi
Abin Shahab
Abreto FU
Ace Tang
Addam Hardy
Adolfo Ochagavía
Adrian Plata
Adrien Duermael
Adrien Folie
Adyanth Hosavalike
Ahmet Alp Balkan
Aidan Feldman
Aidan Hobson Sayers
AJ Bowen
Akhil Mohan
Akihiro Suda
Akim Demaille
Alan Thompson
Alano Terblanche
Albert Callarisa
Alberto Roura
Albin Kerouanton
Aleksa Sarai
Aleksander Piotrowski
Alessandro Boch
Alex Couture-Beil
Alex Mavrogiannis
Alex Mayer
Alexander Boyd
Alexander Chneerov
Alexander Larsson
Alexander Morozov
Alexander Ryabov
Alexandre González
Alexey Igrychev
Alexis Couvreur
Alfred Landrum
Ali Rostami
Alicia Lauerman
Allen Sun
Allie Sadler
Alvin Deng
Amen Belayneh
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
Amir Goldstein
Amit Krishnan
Amit Shukla
Amy Lindburg
Anca Iordache
Anda Xu
Andrea Luzzardi
Andreas Köhler
Andres G. Aragoneses
Andres Leon Rangel
Andrew France
Andrew He
Andrew Hsu
Andrew Macpherson
Andrew McDonnell
Andrew Po
Andrew-Zipperer
Andrey Petrov
Andrii Berehuliak
André Martins
Andy Goldstein
Andy Rothfusz
Anil Madhavapeddy
Ankush Agarwal
Anne Henmi
Anton Polonskiy
Antonio Murdaca
Antonis Kalipetis
Anusha Ragunathan
Ao Li
Arash Deshmeh
Archimedes Trajano
Arko Dasgupta
Arnaud Porterie
Arnaud Rebillout
Arthur Flageul
Arthur Peka
Ashly Mathew
Ashwini Oruganti
Aslam Ahemad
Austin Vazquez
Azat Khuyiyakhmetov
Bardia Keyoumarsi
Barnaby Gray
Bastiaan Bakker
BastianHofmann
Ben Bodenmiller
Ben Bonnefoy
Ben Creasy
Ben Firshman
Benjamin Boudreau
Benjamin Böhmke
Benjamin Nater
Benoit Sigoure
Bhumika Bayani
Bill Wang
Bin Liu
Bingshen Wang
Bishal Das
Bjorn Neergaard
Boaz Shuster
Boban Acimovic
Bogdan Anton
Boris Pruessmann
Brad Baker
Bradley Cicenas
Brandon Mitchell
Brandon Philips
Brent Salisbury
Bret Fisher
Brian (bex) Exelbierd
Brian Goff
Brian Tracy
Brian Wieder
Bruno Sousa
Bryan Bess
Bryan Boreham
Bryan Murphy
bryfry
Calvin Liu
Cameron Spear
Cao Weiwei
Carlo Mion
Carlos Alexandro Becker
Carlos de Paula
carsontham
Carston Schilds
Casey Korver
Ce Gao
Cedric Davies
Cesar Talledo
Cezar Sa Espinola
Chad Faragher
Chao Wang
Charles Chan
Charles Law
Charles Smith
Charlie Drage
Charlotte Mach
ChaYoung You
Chee Hau Lim
Chen Chuanliang
Chen Hanxiao
Chen Mingjie
Chen Qiu
Chris Chinchilla
Chris Couzens
Chris Gavin
Chris Gibson
Chris McKinnel
Chris Snow
Chris Vermilion
Chris Weyl
Christian Persson
Christian Stefanescu
Christophe Robin
Christophe Vidal
Christopher Biscardi
Christopher Crone
Christopher Jones
Christopher Petito <47751006+krissetto@users.noreply.github.com>
Christopher Petito
Christopher Svensson
Christy Norman
Chun Chen
Clinton Kitson
Coenraad Loubser
Colin Hebert
Collin Guarino
Colm Hally
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
Conner Crosby
Corey Farrell
Corey Quon
Cory Bennet
Cory Snider
Craig Osterhout
Craig Wilhite
Cristian Staretu
Daehyeok Mun
Dafydd Crosby
Daisuke Ito
dalanlan
Damien Nadé
Dan Cotora
Dan Wallis
Danial Gharib
Daniel Artine
Daniel Cassidy
Daniel Dao
Daniel Farrell
Daniel Gasienica
Daniel Goosen
Daniel Helfand
Daniel Hiltgen
Daniel J Walsh
Daniel Nephin
Daniel Norberg
Daniel Watkins
Daniel Zhang
Daniil Nikolenko
Danny Berger
Darren Shepherd
Darren Stahl
Dattatraya Kumbhar
Dave Goodchild
Dave Henderson
Dave Tucker
David Alvarez
David Beitey
David Calavera
David Cramer
David Dooling
David Gageot
David Karlsson
David le Blanc
David Lechner
David Scott
David Sheets
David Williamson
David Xia
David Young
Deng Guangxing
Denis Defreyne
Denis Gladkikh
Denis Ollier
Dennis Docter
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek McGowan
Des Preston
Deshi Xiao
Dharmit Shah
Dhawal Yogesh Bhanushali
Dieter Reuter
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
Dima Stopel
Dimitry Andric
Ding Fei
Diogo Monica
Djordje Lukic
Dmitriy Fishman
Dmitry Gusev
Dmitry Smirnov
Dmitry V. Krivenok
Dominik Braun
Don Kjer
Dong Chen
DongGeon Lee
Doug Davis