diff --git a/build.env b/build.env index ea7f3a0..a7446f7 100644 --- a/build.env +++ b/build.env @@ -57,3 +57,10 @@ APT_PROXY= # remote image repository prefix to push to # e.g. ghcr.io/monok8s IMAGE_REPOSITORY= + +# Mirror +# You can host your local mirror by running (but you'll need to download them first) +# kubectl apply -f devtools/dep-pkg-mirror.yaml +# devtools/push-dep-pkg-mirror.sh +# e.g. http://dep-pkg-mirror.default.svc.cluster.local/monok8s +DEP_PKG_MIRROR= diff --git a/devtools/dep-pkg-mirror.yaml b/devtools/dep-pkg-mirror.yaml new file mode 100644 index 0000000..9181ae5 --- /dev/null +++ b/devtools/dep-pkg-mirror.yaml @@ -0,0 +1,101 @@ +# Hosts a mirror for dep pkg +# kubectl apply -f dep-pkg-mirror.yaml -n [namespace] +# kubectl -n [namespace] cp ./packages \ deploy/monok8s-mirror:/usr/share/nginx/html/monok8s/ +# Fetch helper contract: +# DEP_PKG_MIRROR=https://mirror.example.com/monok8s +# mirror URL = ${DEP_PKG_MIRROR}/packages/${mirror_path} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dep-pkg-mirror-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dep-pkg-mirror-nginx +data: + default.conf: | + server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + + location /monok8s/packages/ { + try_files $uri =404; + } + + location = /healthz { + access_log off; + return 200 "ok\n"; + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dep-pkg-mirror +spec: + replicas: 1 + selector: + matchLabels: + app: dep-pkg-mirror + template: + metadata: + labels: + app: dep-pkg-mirror + spec: + containers: + - name: nginx + image: nginx:1.27-alpine + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /healthz + port: http + livenessProbe: + httpGet: + path: /healthz + port: http + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html/monok8s/packages + subPath: packages + readOnly: false + - name: nginx-conf + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: dep-pkg-mirror-data + - name: nginx-conf + configMap: + name: dep-pkg-mirror-nginx +--- +apiVersion: v1 +kind: Service +metadata: + name: dep-pkg-mirror +spec: + type: ClusterIP + selector: + app: dep-pkg-mirror + ports: + - name: http + port: 80 + targetPort: http diff --git a/devtools/push-dep-pkg-mirror.sh b/devtools/push-dep-pkg-mirror.sh new file mode 100755 index 0000000..bbe6302 --- /dev/null +++ b/devtools/push-dep-pkg-mirror.sh @@ -0,0 +1,80 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +NAMESPACE="${NAMESPACE:-default}" +PACKAGES_DIR="$(realpath "$SCRIPT_DIR/../packages/")" +APP_LABEL="${APP_LABEL:-app=dep-pkg-mirror}" +CONTAINER="${CONTAINER:-nginx}" +REMOTE_DIR="${REMOTE_DIR:-/usr/share/nginx/html/monok8s/packages}" + +if [ ! -d "$PACKAGES_DIR" ]; then + echo "error: package dir not found: $PACKAGES_DIR" >&2 + exit 1 +fi + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: missing command: $1" >&2 + exit 1 + } +} + +need_cmd kubectl +need_cmd mktemp +need_cmd tar +need_cmd cp +need_cmd find + +pod="$( + kubectl -n "$NAMESPACE" get pod \ + -l "$APP_LABEL" \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' +)" + +if [ -z "$pod" ]; then + echo "error: no running pod found with label: $APP_LABEL in namespace: $NAMESPACE" >&2 + exit 1 +fi + +echo "using pod: $pod" +echo "remote dir: $REMOTE_DIR" + +echo "checking remote dir is writable" +kubectl -n "$NAMESPACE" exec "$pod" -c "$CONTAINER" -- sh -c " + mkdir -p '$REMOTE_DIR' && + touch '$REMOTE_DIR/.write-test' && + rm -f '$REMOTE_DIR/.write-test' +" + +stage="$(mktemp -d)" +cleanup() { + rm -rf "$stage" +} +trap cleanup EXIT INT TERM + +mkdir -p "$stage/packages" + +echo "staging $PACKAGES_DIR/ as packages/" +tar \ + --exclude='.DS_Store' \ + --exclude='.stamp-*' \ + -C "$PACKAGES_DIR" \ + -cf - . | tar -C "$stage/packages" -xf - + +echo "copying staged packages into mirror pod" +kubectl -n "$NAMESPACE" cp \ + "$stage/packages/." \ + "$pod:$REMOTE_DIR" \ + -c "$CONTAINER" + +echo "done" +echo +echo "Mirror base URL:" +echo " DEP_PKG_MIRROR=http://dep-pkg-mirror.${NAMESPACE}.svc.cluster.local/monok8s" +echo +echo "Example:" +echo " packages/kubernetes/kubelet-v1.35.3" +echo " -> http://dep-pkg-mirror.${NAMESPACE}.svc.cluster.local/monok8s/packages/kubernetes/kubelet-v1.35.3" diff --git a/docker/download-packages.Dockerfile b/docker/download-packages.Dockerfile index 94919b1..2b5242c 100644 --- a/docker/download-packages.Dockerfile +++ b/docker/download-packages.Dockerfile @@ -1,13 +1,24 @@ +ARG DEP_PKG_MIRROR= +ARG DEP_PKG_OFFLINE=0 + FROM alpine:3.23.0 AS base +ARG DEP_PKG_MIRROR +ARG DEP_PKG_OFFLINE +ENV DEP_PKG_MIRROR="${DEP_PKG_MIRROR}" +ENV DEP_PKG_OFFLINE="${DEP_PKG_OFFLINE}" RUN apk add --no-cache curl ca-certificates +COPY scripts/fetch-artifact /usr/local/bin/fetch-artifact + # ---- kubelet ---- FROM base AS kubelet ARG KUBE_VERSION ARG ARCH WORKDIR /out/kubernetes -RUN curl -fL --retry 3 -o "kubelet-${KUBE_VERSION}" \ - "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubelet" && \ +RUN fetch-artifact \ + "packages/kubernetes/kubelet-${KUBE_VERSION}" \ + "kubelet-${KUBE_VERSION}" \ + "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubelet" && \ chmod +x "kubelet-${KUBE_VERSION}" # ---- kubeadm ---- @@ -15,8 +26,10 @@ FROM base AS kubeadm ARG KUBE_VERSION ARG ARCH WORKDIR /out/kubernetes -RUN curl -fL --retry 3 -o "kubeadm-${KUBE_VERSION}" \ - "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubeadm" && \ +RUN fetch-artifact \ + "packages/kubernetes/kubeadm-${KUBE_VERSION}" \ + "kubeadm-${KUBE_VERSION}" \ + "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubeadm" && \ chmod +x "kubeadm-${KUBE_VERSION}" # ---- kubectl ---- @@ -24,136 +37,190 @@ FROM base AS kubectl ARG KUBE_VERSION ARG ARCH WORKDIR /out/kubernetes -RUN curl -fL --retry 3 -o "kubectl-${KUBE_VERSION}" \ - "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubectl" && \ +RUN fetch-artifact \ + "packages/kubernetes/kubectl-${KUBE_VERSION}" \ + "kubectl-${KUBE_VERSION}" \ + "https://dl.k8s.io/${KUBE_VERSION}/bin/linux/${ARCH}/kubectl" && \ chmod +x "kubectl-${KUBE_VERSION}" # ---- busybox ---- FROM base AS busybox ARG BUSYBOX_VERSION +ARG BUSYBOX_TAR WORKDIR /out -RUN curl -fL --retry 3 -o "busybox-${BUSYBOX_VERSION}.tar.gz" \ - "https://github.com/mirror/busybox/archive/refs/tags/${BUSYBOX_VERSION}.tar.gz" +RUN fetch-artifact \ + "${BUSYBOX_TAR}" \ + "busybox-${BUSYBOX_VERSION}.tar.gz" \ + "https://github.com/mirror/busybox/archive/refs/tags/${BUSYBOX_VERSION}.tar.gz" # ---- e2fsprogs ---- FROM base AS e2fsprogs ARG E2FSPROGS_VERSION +ARG E2FSPROGS_TAR WORKDIR /out -RUN curl -fL --retry 3 -o "e2fsprogs-v${E2FSPROGS_VERSION}.tar.gz" \ - "https://github.com/tytso/e2fsprogs/archive/refs/tags/v${E2FSPROGS_VERSION}.tar.gz" +RUN fetch-artifact \ + "${E2FSPROGS_TAR}" \ + "e2fsprogs-v${E2FSPROGS_VERSION}.tar.gz" \ + "https://github.com/tytso/e2fsprogs/archive/refs/tags/v${E2FSPROGS_VERSION}.tar.gz" # ---- dpdk ---- FROM base AS dpdk ARG DPDK_VERSION +ARG DPDK_TAR WORKDIR /out/nxp/dpdk -RUN curl -fL --retry 3 -o "${DPDK_VERSION}.tar.gz" \ - "https://github.com/nxp-qoriq/dpdk/archive/refs/tags/${DPDK_VERSION}.tar.gz" +RUN fetch-artifact \ + "${DPDK_TAR}" \ + "${DPDK_VERSION}.tar.gz" \ + "https://github.com/nxp-qoriq/dpdk/archive/refs/tags/${DPDK_VERSION}.tar.gz" # ---- fmlib ---- FROM base AS fmlib ARG FMLIB_VERSION +ARG FMLIB_TAR WORKDIR /out/nxp/fmlib -RUN curl -fL --retry 3 -o "${FMLIB_VERSION}.tar.gz" \ - "https://github.com/nxp-qoriq/fmlib/archive/refs/tags/${FMLIB_VERSION}.tar.gz" +RUN fetch-artifact \ + "${FMLIB_TAR}" \ + "${FMLIB_VERSION}.tar.gz" \ + "https://github.com/nxp-qoriq/fmlib/archive/refs/tags/${FMLIB_VERSION}.tar.gz" # ---- fmc ---- FROM base AS fmc ARG FMC_VERSION +ARG FMC_TAR WORKDIR /out/nxp/fmc -RUN curl -fL --retry 3 -o "${FMC_VERSION}.tar.gz" \ - "https://github.com/nxp-qoriq/fmc/archive/refs/tags/${FMC_VERSION}.tar.gz" +RUN fetch-artifact \ + "${FMC_TAR}" \ + "${FMC_VERSION}.tar.gz" \ + "https://github.com/nxp-qoriq/fmc/archive/refs/tags/${FMC_VERSION}.tar.gz" # ---- vpp ---- FROM base AS vpp ARG VPP_VERSION +ARG VPP_TAR WORKDIR /out/nxp/vpp -RUN curl -fL --retry 3 -o "${VPP_VERSION}.tar.gz" \ - "https://github.com/nxp-qoriq/vpp/archive/refs/tags/${VPP_VERSION}.tar.gz" +RUN fetch-artifact \ + "${VPP_TAR}" \ + "${VPP_VERSION}.tar.gz" \ + "https://github.com/nxp-qoriq/vpp/archive/refs/tags/${VPP_VERSION}.tar.gz" # ---- MUSL CC ---- FROM base AS aarch64_musl_cc WORKDIR /out -RUN curl -fL --retry 3 -o "aarch64-linux-musl-cross.tgz" \ - "https://musl.cc/aarch64-linux-musl-cross.tgz" +RUN fetch-artifact \ + "packages/aarch64-linux-musl-cross.tgz" \ + "aarch64-linux-musl-cross.tgz" \ + "https://musl.cc/aarch64-linux-musl-cross.tgz" # ---- ASK ---- FROM base AS mono_ask ARG MONO_ASK_VERSION +ARG MONO_ASK_TAR WORKDIR /out/ask -RUN curl -fL --retry 3 -o "${MONO_ASK_VERSION}.tar.gz" \ - "https://github.com/we-are-mono/ASK/archive/refs/tags/${MONO_ASK_VERSION}.tar.gz" +RUN fetch-artifact \ + "${MONO_ASK_TAR}" \ + "${MONO_ASK_VERSION}.tar.gz" \ + "https://github.com/we-are-mono/ASK/archive/refs/tags/${MONO_ASK_VERSION}.tar.gz" # ---- libnfnetlink ---- FROM base AS libnfnetlink ARG LIBNFNETLINK_VERSION +ARG LIBNFNETLINK_TAR WORKDIR /out/ask/libnfnetlink -RUN curl -fL --retry 3 -o "${LIBNFNETLINK_VERSION}.tar.bz2" \ - "https://www.netfilter.org/projects/libnfnetlink/files/libnfnetlink-${LIBNFNETLINK_VERSION}.tar.bz2" +RUN fetch-artifact \ + "${LIBNFNETLINK_TAR}" \ + "${LIBNFNETLINK_VERSION}.tar.bz2" \ + "https://www.netfilter.org/projects/libnfnetlink/files/libnfnetlink-${LIBNFNETLINK_VERSION}.tar.bz2" # ---- libnfct ---- FROM base AS libnfct ARG LIBNFCT_VERSION +ARG LIBNFCT_TAR WORKDIR /out/ask/libnfct -RUN curl -fL --retry 3 -o "${LIBNFCT_VERSION}.tar.xz" \ - "https://www.netfilter.org/projects/libnetfilter_conntrack/files/libnetfilter_conntrack-${LIBNFCT_VERSION}.tar.xz" +RUN fetch-artifact \ + "${LIBNFCT_VERSION_TAR}" \ + "${LIBNFCT_VERSION}.tar.xz" \ + "https://www.netfilter.org/projects/libnetfilter_conntrack/files/libnetfilter_conntrack-${LIBNFCT_VERSION}.tar.xz" # ---- libmnl ---- FROM base AS libmnl ARG LIBMNL_VERSION +ARG LIBMNL_TAR WORKDIR /out/ask/libmnl -RUN curl -fL --retry 3 -o "${LIBMNL_VERSION}.tar.bz2" \ - "https://www.netfilter.org/projects/libmnl/files/libmnl-${LIBMNL_VERSION}.tar.bz2" +RUN fetch-artifact \ + "${LIBMNL_TAR}" \ + "${LIBMNL_VERSION}.tar.bz2" \ + "https://www.netfilter.org/projects/libmnl/files/libmnl-${LIBMNL_VERSION}.tar.bz2" # ---- tclap ---- FROM base AS tclap ARG TCLAP_VERSION +ARG TCLAP_TAR WORKDIR /out/ask/tclap -RUN curl -fL --retry 3 -o "${TCLAP_VERSION}.tar.gz" \ - "https://sourceforge.net/projects/tclap/files/tclap-${TCLAP_VERSION}.tar.gz" +RUN fetch-artifact \ + "${TCLAP_TAR}" \ + "${TCLAP_VERSION}.tar.gz" \ + "https://sourceforge.net/projects/tclap/files/tclap-${TCLAP_VERSION}.tar.gz" # ---- libxml2 ---- FROM base AS libxml2 ARG LIBXML2_VERSION +ARG LIBXML2_TAR WORKDIR /out/ask/libxml2 -RUN curl -fL --retry 3 -o "${LIBXML2_VERSION}.tar.xz" \ - "https://download.gnome.org/sources/libxml2/2.11/libxml2-${LIBXML2_VERSION}.tar.xz" +RUN fetch-artifact \ + "${LIBXML2_TAR}" \ + "${LIBXML2_VERSION}.tar.xz" \ + "https://download.gnome.org/sources/libxml2/2.11/libxml2-${LIBXML2_VERSION}.tar.xz" # ---- libcli ---- FROM base AS libcli ARG LIBCLI_VERSION +ARG LIBCLI_TAR WORKDIR /out/ask/libcli -RUN curl -fL --retry 3 -o "${LIBCLI_VERSION}.tar.gz" \ - "https://github.com/dparrish/libcli/archive/refs/tags/V${LIBCLI_VERSION}.tar.gz" +RUN fetch-artifact \ + "${LIBCLI_TAR}" \ + "${LIBCLI_VERSION}.tar.gz" \ + "https://github.com/dparrish/libcli/archive/refs/tags/V${LIBCLI_VERSION}.tar.gz" # ---- libpcap ---- FROM base AS libpcap ARG LIBPCAP_VERSION +ARG LIBPCAP_TAR WORKDIR /out/ask/libpcap -RUN curl -fL --retry 3 -o "${LIBPCAP_VERSION}.tar.xz" \ - "https://www.tcpdump.org/release/libpcap-${LIBPCAP_VERSION}.tar.xz" +RUN fetch-artifact \ + "${LIBPCAP_TAR}" \ + "${LIBPCAP_VERSION}.tar.xz" \ + "https://www.tcpdump.org/release/libpcap-${LIBPCAP_VERSION}.tar.xz" # ---- alpine rootfs ---- FROM base AS alpine_rootfs ARG ALPINE_SERIES ARG ALPINE_ARCH -ARG ALPINE_VER +ARG ALPINE_TAR WORKDIR /out -RUN curl -fL --retry 3 -o "alpine-minirootfs-${ALPINE_VER}-${ALPINE_ARCH}.tar.gz" \ - "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_SERIES}/releases/${ALPINE_ARCH}/alpine-minirootfs-${ALPINE_VER}-${ALPINE_ARCH}.tar.gz" +RUN fetch-artifact \ + "${ALPINE_TAR}" \ + "alpine-minirootfs-${ALPINE_VER}-${ALPINE_ARCH}.tar.gz" \ + "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_SERIES}/releases/${ALPINE_ARCH}/alpine-minirootfs-${ALPINE_VER}-${ALPINE_ARCH}.tar.gz" # ---- nxp linux ---- FROM base AS nxp_linux ARG NXP_VERSION +ARG NXP_TAR WORKDIR /out/nxp/kernel -RUN curl -fL --retry 3 -o "${NXP_VERSION}.tar.gz" \ - "https://github.com/nxp-qoriq/linux/archive/refs/tags/${NXP_VERSION}.tar.gz" +RUN fetch-artifact \ + "${NXP_TAR}" \ + "${NXP_VERSION}.tar.gz" \ + "https://github.com/nxp-qoriq/linux/archive/refs/tags/${NXP_VERSION}.tar.gz" # ---- crio ---- FROM base AS crio ARG CRIO_VERSION +ARG CRIO_TAR WORKDIR /out -RUN curl -fL --retry 3 -o "${CRIO_VERSION}.tar.gz" \ - "https://storage.googleapis.com/cri-o/artifacts/${CRIO_VERSION}.tar.gz" +RUN fetch-artifact \ + "${CRIO_TAR}" \ + "${CRIO_VERSION}.tar.gz" \ + "https://storage.googleapis.com/cri-o/artifacts/${CRIO_VERSION}.tar.gz" # ---- final exported artifact set ---- FROM scratch diff --git a/makefile b/makefile index 8c7c48b..a3b7995 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,7 @@ TAG ?= dev PACKAGES_DIR := packages OUT_DIR := out -E2FSPROGS_TAR := $(PACKAGES_DIR)/e2fsprogs-$(E2FSPROGS_VERSION).tar.gz +E2FSPROGS_TAR := $(PACKAGES_DIR)/e2fsprogs-v$(E2FSPROGS_VERSION).tar.gz BUSYBOX_TAR := $(PACKAGES_DIR)/busybox-$(BUSYBOX_VERSION).tar.gz ALPINE_TAR := $(PACKAGES_DIR)/alpine-minirootfs-$(ALPINE_VER)-$(ALPINE_ARCH).tar.gz NXP_TAR := $(PACKAGES_DIR)/nxp/kernel/$(NXP_VERSION).tar.gz @@ -127,6 +127,7 @@ $(OUT_DIR): $(DOWNLOAD_PACKAGES_STAMP): docker/download-packages.Dockerfile build.env makefile | $(PACKAGES_DIR) docker build \ -f docker/download-packages.Dockerfile \ + --build-arg DEP_PKG_MIRROR=$(DEP_PKG_MIRROR) \ --build-arg KUBE_VERSION=$(KUBE_VERSION) \ --build-arg ARCH=$(ARCH) \ --build-arg BUSYBOX_VERSION=$(BUSYBOX_VERSION) \ @@ -149,6 +150,23 @@ $(DOWNLOAD_PACKAGES_STAMP): docker/download-packages.Dockerfile build.env makefi --build-arg ALPINE_VER=$(ALPINE_VER) \ --build-arg NXP_VERSION=$(NXP_VERSION) \ --build-arg CRIO_VERSION=$(CRIO_VERSION) \ + --build-arg BUSYBOX_TAR=$(BUSYBOX_TAR) \ + --build-arg E2FSPROGS_TAR=$(E2FSPROGS_TAR) \ + --build-arg DPDK_TAR=$(DPDK_TAR) \ + --build-arg FMLIB_TAR=$(FMLIB_TAR) \ + --build-arg FMC_TAR=$(FMC_TAR) \ + --build-arg VPP_TAR=$(VPP_TAR) \ + --build-arg MONO_ASK_TAR=$(MONO_ASK_TAR) \ + --build-arg LIBNFNETLINK_TAR=$(LIBNFNETLINK_TAR) \ + --build-arg LIBMNL_TAR=$(LIBMNL_TAR) \ + --build-arg LIBNFCT_TAR=$(LIBNFCT_TAR) \ + --build-arg LIBCLI_TAR=$(LIBCLI_TAR) \ + --build-arg LIBXML2_TAR=$(LIBXML2_TAR) \ + --build-arg LIBPCAP_TAR=$(LIBPCAP_TAR) \ + --build-arg TCLAP_TAR=$(TCLAP_TAR) \ + --build-arg ALPINE_TAR=$(ALPINE_TAR) \ + --build-arg NXP_TAR=$(NXP_TAR) \ + --build-arg CRIO_TAR=$(CRIO_TAR) \ --output type=local,dest=./$(PACKAGES_DIR) . @touch $@ diff --git a/scripts/fetch-artifact b/scripts/fetch-artifact new file mode 100755 index 0000000..a712355 --- /dev/null +++ b/scripts/fetch-artifact @@ -0,0 +1,40 @@ +#!/bin/sh +set -eu + +# Fetch helper contract: +# DEP_PKG_MIRROR=https://mirror.example.com/monok8s +# mirror URL = ${DEP_PKG_MIRROR}/${mirror_path} + +if [ "$#" -ne 3 ]; then + echo "usage: fetch-artifact " >&2 + exit 2 +fi + +mirror_path="$1" +out="$2" +upstream_url="$3" + +mkdir -p "$(dirname "$out")" +rm -f "$out" + +if [ -n "${DEP_PKG_MIRROR:-}" ]; then + mirror_url="${DEP_PKG_MIRROR%/}/${mirror_path}" + echo "fetch-artifact: trying mirror: ${mirror_url}" >&2 + if curl -fL --retry 3 -o "$out" "$mirror_url"; then + exit 0 + fi + rm -f "$out" + + if [ "${DEP_PKG_OFFLINE:-0}" = "1" ]; then + echo "fetch-artifact: mirror miss and DEP_PKG_OFFLINE=1: ${mirror_url}" >&2 + exit 1 + fi +fi + +if [ "${DEP_PKG_OFFLINE:-0}" = "1" ]; then + echo "fetch-artifact: DEP_PKG_OFFLINE=1 and no usable mirror for ${mirror_path}" >&2 + exit 1 +fi + +echo "fetch-artifact: fetching upstream: ${upstream_url}" >&2 +curl -fL --retry 3 -o "$out" "$upstream_url"