diff --git a/.circleci/config.yml b/.circleci/config.yml index fb5af0b..c2766d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,3 +1,4 @@ +#generated via: polly generate --mirror=circleci --registry=ghcr.io/unhookd > .circleci/config.yml --- workflows: version: 2 @@ -8,15 +9,15 @@ workflows: version: 2 jobs: primary: - working_directory: "/home/app/polly" + #working_directory: "/home/app/polly" steps: - checkout - run: name: primary command: |- - echo DEMO!!!! bundle config set --local path vendor/bundle bundle install bundle exec rspec + polly help docker: - - image: ghcr.io/unhookd/polly:3.0-rc1 + - image: ghcr.io/unhookd/polly:4.0-rc-3 diff --git a/.dockerignore b/.dockerignore index e7d4494..b3cf2fc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,7 @@ vendor .circleci .github Dockerfile -Pollyfile Dockerfile.default .dockerignore Gemfile +*.swp diff --git a/.github/workflows/primary.yml b/.github/workflows/primary.yml index 67de6fc..b55022e 100644 --- a/.github/workflows/primary.yml +++ b/.github/workflows/primary.yml @@ -16,8 +16,8 @@ jobs: container: options: "--user 0" - #image: ubuntu:jammy-20220421 - image: ghcr.io/unhookd/polly:3.0-rc1@sha256:7cc65086b101fe352a8ff83180888f0f7079b2f80139b0f39ba7b2a4cb34b168 + image: public.ecr.aws/debian/debian:bookworm-20240311-slim + #image: ghcr.io/unhookd/polly:4.0-rc-3@sha256:1169fa172ec82abfaa368396aaad8be59d1879e76fcd115495567598d7500586 steps: - uses: actions/checkout@v3 @@ -26,6 +26,12 @@ jobs: run: | set -ex + export DEBIAN_FRONTEND="noninteractive" + export LC_ALL="C.UTF-8" + export LANG="en_US" + export LANGUAGE="en_US" + export ACCEPT_EULA="y" + (getent group alpha || groupadd --gid 121 alpha); (getent group tau || groupadd --gid 123 tau); (getent group beta || groupadd --gid 134 beta); @@ -36,15 +42,16 @@ jobs: (getent passwd runner || useradd --uid 1001 --home-dir /home/runner --create-home --shell /bin/bash runner --groups alpha,beta,docker,theta,zeta,tau); apt-get update; apt-get install -y locales locales-all; apt-get clean; test -e /usr/lib/locale/locale-archive || ((locale-gen --purge en_US); (echo -e "LANG=$LANG\nLANGUAGE=$LANGUAGE\n" | tee /etc/default/locale); (locale-gen $LANGUAGE); (dpkg-reconfigure locales)); - apt-get update; apt-get install -y vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper; apt-get clean; - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add; - apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"; + apt-get update; apt-get install -y vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper procps; apt-get clean; + test -e /etc/apt/keyrings/kubernetes-apt-keyring.gpg || (curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | gpg --batch --no-tty --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg); + echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list; apt-get update; apt-get install -y kubectl; apt-get clean; usermod -a -G $(grep docker /etc/group | cut -d: -f3) app; usermod -a -G $(grep docker /etc/group | cut -d: -f3) runner; + mkdir /tmp/buildkit && cd /tmp/buildkit && curl -sL -o buildkit.tar.gz "https://github.com/moby/buildkit/releases/download/v0.13.1/buildkit-v0.13.1.linux-amd64.tar.gz" && tar zxf buildkit.tar.gz && mv bin/buildctl /usr/local/bin && cd && rm -Rf /tmp/buildkit; mkdir -p /polly/safe/git /polly/safe/run /polly/safe/tmp /polly/app /app /__w/polly/polly; - chown -R app.alpha /home/app /polly /app /__w/polly/polly; + chown -R app:alpha /home/app /polly /app /__w/polly/polly; chown -R app /home/app /polly /app /__w/polly/polly; chown -R app /home/app; su app -s /bin/bash -c 'cd /__w/polly/polly && gem build polly.gemspec -o /home/app/polly-latest.gem'; diff --git a/.gitignore b/.gitignore index 96cfcb2..7103391 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .bundle vendor Gemfile.lock +Dockerfile-pollygen* +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab86df..0bda8a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +# [4.31.0] - 2024-04-12 - Jon Bardin + +Add `polly tags` and initial prototype of build,test,deploy gitch ci + +####### + +# [4.30.0] - 2024-04-10 - Jon Bardin + +better async output + +####### + +# [4.29.0] - 2024-04-10 - Jon Bardin + +new version bits + +####### + +# [4.28.0] - 2024-04-01 - Jon Bardin + +Upgrade base images and to ruby3 + +####### + +# [4.27.0] - 2024-02-07 - Jon Bardin + +Merge build features + +####### + +# [4.26.0] - 2023-05-19 - Jon + +add more build/run options + +####### + +# [4.25.0] - 2023-05-16 - Jon Bardin + +Allow `host-aliases` block to be defined and written out to /etc/hosts via cloud-init + +####### + +# [4.24.0] - 2023-05-16 - Jon Bardin + +repair wxh git push hooks + +####### + +# [4.23.0] - 2023-05-16 - Jon Bardin + +Add better version support + +####### + +# [2.22.0] - 2023-05-16 - Jon Bardin + + + +####### + +# [4.0.1] - 2023-05-16 - Jon Bardin + +Add support for multi-platform launch configuration + +####### + # [2.20.0] - 2023-01-22 - Jon Bardin Test image container diff diff --git a/Dockerfile.default b/Dockerfile.default index 0e6361c..11b612c 100644 --- a/Dockerfile.default +++ b/Dockerfile.default @@ -1,5 +1,5 @@ # syntax=docker/dockerfile-upstream:master-experimental -FROM ghcr.io/unhookd/polly:3.0-rc1@sha256:7cc65086b101fe352a8ff83180888f0f7079b2f80139b0f39ba7b2a4cb34b168 AS base +FROM public.ecr.aws/debian/debian:bookworm-20240311-slim AS base USER root ENV DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=en_US LANGUAGE=en_US ACCEPT_EULA=y RUN set -ex; \ @@ -13,12 +13,13 @@ RUN set -ex; \ (getent passwd runner || useradd --uid 1001 --home-dir /home/runner --create-home --shell /bin/bash runner --groups alpha,beta,docker,theta,zeta,tau); \ apt-get update; apt-get install -y locales locales-all; apt-get clean; \ test -e /usr/lib/locale/locale-archive || ((locale-gen --purge en_US); (echo -e "LANG=$LANG\nLANGUAGE=$LANGUAGE\n" | tee /etc/default/locale); (locale-gen $LANGUAGE); (dpkg-reconfigure locales)); \ - apt-get update; apt-get install -y vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper; apt-get clean; \ - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add; \ - apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"; \ + apt-get update; apt-get install -y vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper procps; apt-get clean; \ + test -e /etc/apt/keyrings/kubernetes-apt-keyring.gpg || (curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | gpg --batch --no-tty --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg); \ + echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list; \ apt-get update; apt-get install -y kubectl; apt-get clean; \ usermod -a -G $(grep docker /etc/group | cut -d: -f3) app; \ usermod -a -G $(grep docker /etc/group | cut -d: -f3) runner; \ + mkdir /tmp/buildkit && cd /tmp/buildkit && curl -sL -o buildkit.tar.gz "https://github.com/moby/buildkit/releases/download/v0.13.1/buildkit-v0.13.1.linux-amd64.tar.gz" && tar zxf buildkit.tar.gz && mv bin/buildctl /usr/local/bin && cd && rm -Rf /tmp/buildkit; \ true; # syntax=docker/dockerfile-upstream:master-experimental FROM base AS gem @@ -32,7 +33,7 @@ COPY --chown=app bin /__w/polly/polly/bin COPY --chown=app doc /__w/polly/polly/doc RUN set -ex; \ mkdir -p /polly/safe/git /polly/safe/run /polly/safe/tmp /polly/app /app /__w/polly/polly; \ - chown -R app.alpha /home/app /polly /app /__w/polly/polly; \ + chown -R app:alpha /home/app /polly /app /__w/polly/polly; \ chown -R app /home/app /polly /app /__w/polly/polly; \ chown -R app /home/app; \ su app -s /bin/bash -c 'cd /__w/polly/polly && gem build polly.gemspec -o /home/app/polly-latest.gem'; \ @@ -43,6 +44,12 @@ WORKDIR /home/app COPY --chown=app --from=gem /home/app/polly-latest.gem /home/app/polly-latest.gem RUN set -ex; \ gem install --no-document --minimal-deps /home/app/polly-latest.gem && grep -Rn '\.gem\.' /var/lib 2>/dev/null | cut -d: -f1 | sort | uniq | xargs -I{} rm {} && rm /home/app/polly-latest.gem; \ + ln -sf /usr/local/bin/polly /bin/dockerfile-frontend; \ + true; +COPY --chown=app Pollyfile /home/app/Pollyfile +COPY --chown=app config/git-repo/templates /home/app/config/git-repo/templates +RUN set -ex; \ + mkdir -p /polly/safe/git/polly/hooks && chown -R app:app /polly/safe; \ true; USER app -LABEL "org.opencontainers.image.description"="For pollyci" +ENTRYPOINT ["/bin/dockerfile-frontend"] diff --git a/Gemfile b/Gemfile index 927cd61..174deac 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ gemspec ## https://github.com/rubygems/rubygems/issues/1104 group "development" do - gem "guard-rspec" gem "rake" gem "rspec" end diff --git a/Pollyfile b/Pollyfile index a16e0b4..6b7a268 100644 --- a/Pollyfile +++ b/Pollyfile @@ -1,9 +1,9 @@ #!/usr/bin/env ruby @base = image { - stage "base", "ghcr.io/unhookd/polly:3.0-rc1@sha256:7cc65086b101fe352a8ff83180888f0f7079b2f80139b0f39ba7b2a4cb34b168" - #stage "base", "ghcr.io/unhookd/polly:3.0-rc1" - #stage "base", "ubuntu:jammy-20220421" + #stage "base", "ghcr.io/unhookd/polly:4.0-rc-3@sha256:1169fa172ec82abfaa368396aaad8be59d1879e76fcd115495567598d7500586" + stage "base", "public.ecr.aws/debian/debian:bookworm-20240311-slim" + #stage "base", "ghcr.io/unhookd/polly:4.0-rc-3" root @@ -20,23 +20,27 @@ group(1000, "theta") group(1001, "zeta") - useradd(1000, "app", "alpha,beta,docker,theta,zeta,tau") + useradd(1000, "polly", "alpha,beta,docker,theta,zeta,tau") useradd(1001, "runner", "alpha,beta,docker,theta,zeta,tau") apt %w{locales locales-all} run %q{test -e /usr/lib/locale/locale-archive || ((locale-gen --purge en_US); (echo -e "LANG=$LANG\nLANGUAGE=$LANGUAGE\n" | tee /etc/default/locale); (locale-gen $LANGUAGE); (dpkg-reconfigure locales))} - apt %w{vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper} + apt %w{vim git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby3* libruby3* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper procps} + + #run %q{curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add} + #run %q{apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"} + run %q{test -e /etc/apt/keyrings/kubernetes-apt-keyring.gpg || (curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | gpg --batch --no-tty --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg)} + run %q{echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list} - run %q{curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add} - run %q{apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"} apt %w{kubectl} - run %q{usermod -a -G $(grep docker /etc/group | cut -d: -f3) app} + run %q{usermod -a -G $(grep docker /etc/group | cut -d: -f3) polly} run %q{usermod -a -G $(grep docker /etc/group | cut -d: -f3) runner} - run %q{mkdir /tmp/buildkit && curl -sL -o buildkit.tar.gz "https://github.com/moby/buildkit/releases/download/v0.11.1/buildkit-v0.11.1.linux-amd64.tar.gz" && tar zxf buildkit.tar.gz && mv bin/buildctl /usr/local/bin && rm -Rf /tmp/buildkit} + buildkit_version = "v0.22.0" + run %Q{mkdir /tmp/buildkit && cd /tmp/buildkit && curl -sL -o buildkit.tar.gz "https://github.com/moby/buildkit/releases/download/#{buildkit_version}/buildkit-#{buildkit_version}.linux-amd64.tar.gz" && tar zxf buildkit.tar.gz && mv bin/buildctl /usr/local/bin && cd && rm -Rf /tmp/buildkit} } # image declares container artifacts @@ -52,34 +56,34 @@ #TODO: more prototype-z detection command("COPY") { - "--chown=app polly.gemspec VERSION CHANGELOG.md /__w/polly/polly/" + "--chown=polly polly.gemspec VERSION CHANGELOG.md /__w/polly/polly/" } command("COPY") { - "--chown=app Thorfile /__w/polly/polly/" + "--chown=polly Thorfile /__w/polly/polly/" } command("COPY") { - "--chown=app config /__w/polly/polly/config/" + "--chown=polly config /__w/polly/polly/config/" } command("COPY") { - "--chown=app lib /__w/polly/polly/lib" + "--chown=polly lib /__w/polly/polly/lib" } command("COPY") { - "--chown=app bin /__w/polly/polly/bin" + "--chown=polly bin /__w/polly/polly/bin" } command("COPY") { - "--chown=app doc /__w/polly/polly/doc" + "--chown=polly doc /__w/polly/polly/doc" } - run %q{mkdir -p /polly/app /app /__w/polly/polly} - run %q{chown -R app.alpha /home/app /polly /app /__w/polly/polly} - run %q{chown -R app /home/app /polly /app /__w/polly/polly} - run %q{chown -R app /home/app} - run %q{su app -s /bin/bash -c 'cd /__w/polly/polly && gem build polly.gemspec -o /home/app/polly-latest.gem'} + run %q{mkdir -p /polly/safe/git /polly/safe/run /polly/safe/tmp /polly/polly /polly /__w/polly/polly} + run %q{chown -R polly:alpha /home/polly /polly /polly /__w/polly/polly} + run %q{chown -R polly /home/polly /polly /polly /__w/polly/polly} + run %q{chown -R polly /home/polly} + run %q{su polly -s /bin/bash -c 'cd /__w/polly/polly && gem build polly.gemspec -o /home/polly/polly-latest.gem'} } # final bits are just the .gem install as if on end-user box @@ -87,38 +91,65 @@ stage "deploy", @base.stage command("WORKDIR") { - "/home/app" + "/home/polly" + } + + command("COPY") { + "--chown=polly --from=gem /home/polly/polly-latest.gem /home/polly/polly-latest.gem" + } + + run %q{gem install --no-document --minimal-deps /home/polly/polly-latest.gem && grep -Rn '\.gem\.' /var/lib 2>/dev/null | cut -d: -f1 | sort | uniq | xargs -I{} rm {} && rm /home/polly/polly-latest.gem} + + run %q{ln -sf /usr/local/bin/polly /bin/dockerfile-frontend} + + command("COPY") { + "--chown=polly Pollyfile /home/polly/Pollyfile" } command("COPY") { - "--chown=app --from=gem /home/app/polly-latest.gem /home/app/polly-latest.gem" + "--chown=polly config/git-repo/templates /home/polly/config/git-repo/templates" } - run %q{gem install --no-document --minimal-deps /home/app/polly-latest.gem && grep -Rn '\.gem\.' /var/lib 2>/dev/null | cut -d: -f1 | sort | uniq | xargs -I{} rm {} && rm /home/app/polly-latest.gem} + command("COPY") { + "--chown=polly kubernetes /home/polly/kubernetes" + } + + run %q{mkdir -p /polly/safe/git/polly/hooks && chown -R polly:polly /polly/safe} app + + command("ENTRYPOINT") { + '["/bin/dockerfile-frontend"]' + } } #description("For pollyci") -workflow_image = "ghcr.io/unhookd/polly:3.0-rc1" -#TODO: !!!! make this make sense to configure workflow_image = "polly:latest" +#workflow_image = "ghcr.io/unhookd/polly:3.0-rc1" +#TODO: !!!! make this make sense to configure +#workflow_image = "polly-registry:23443/polly-registry/polly:latest" @plain_workflow = plan { job("primary", - [{"image"=>workflow_image}], [ - {"run"=>{"name"=>"demo","command"=>"echo DEMO!!!!"}}, + #{"run"=>{"name"=>"demo","command"=>"echo DEMO?????"}}, + #{"run"=>{"name"=>"demo","command"=>"sleep 300"}}, + #{"run"=>{"name"=>"demo","command"=>"echo DEMO!!!!"}}, + #{"run"=>{"name"=>"demo","command"=>"polly generate > Dockerfile"}}, + #{"run"=>{"name"=>"demo","command"=>"polly help"}}, + #{"run"=>{"name"=>"demo","command"=>"sleep infinity"}}, + #{"run"=>{"name"=>"demo","command"=>"polly build"}}, {"run"=>{"name"=>"config","command"=>"bundle config set --local path vendor/bundle"}}, {"run"=>{"name"=>"bundler","command"=>"bundle install"}}, {"run"=>{"name"=>"rspec","command"=>"bundle exec rspec"}}, - {"run"=>{"name"=>"build","command"=>"buildctl --addr kube-pod://buildkitd build --frontend dockerfile.v0 --local context=. --local dockerfile=."}} - ],{},"/home/app/polly" + {"run"=>{"name"=>"demo","command"=>"polly help"}}, + #{"run"=>{"name"=>"build","command"=>"pwd && ls -l && buildctl --timeout 120 --addr tcp://polly-buildkitd:1234 --tlsservername polly-buildkitd --tlscacert /certs/client/ca.pem --tlscert /certs/client/cert.pem --tlskey /certs/client/key.pem build --frontend dockerfile.v0 --local context=. --local dockerfile=."}} + ],{},"/home/polly/polly" ) } @gitch_pipeline = continuous { - #TODO: !!!! - #publish @bootstrap_artifact + build @bootstrap_artifact test @plain_workflow + deploy @example_instance } diff --git a/README.md b/README.md index 910c151..cad422d 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,18 @@ Do not expose polly to a kubernetes cluster unless you have thoroughly understoo TODO, in the future, `polly` will be available via `sudo gem install polly` ... until then it must be installed manually, see below + #### deps sudo apt-get install ruby rubygems-integration libffi-dev build-essential --no-install-recommends sudo gem install bundler - cd ~/workspace + + #### clone repo git clone git@github.com:unhookd/polly.git cd polly + + #### install app bundle config set --local path vendor/bundle bundle install - sudo ln -fs ${HOME}/workspace/polly/bin/polly /usr/local/bin/polly + sudo ln -fs $(realpath bin/polly) /usr/local/bin/polly polly help # .circleci/config.yml @@ -116,12 +120,6 @@ exec's into the polly controller deployement to provide a debugging interactive prints logs of polly controller deployement for debugging -# polly docker-config - -accepts on STDIN a `~/.docker/config.json` document, and creates a specific secret for allowin fetching from private repos in private clusters. - -TBD: allow STDIN creation of a variety of configMap/secretMap resources (SEE: `polly certificate`) - # polly key TBD: manages authentication diff --git a/VERSION b/VERSION index a4cc673..e8d959f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.20.0 \ No newline at end of file +4.31.0 \ No newline at end of file diff --git a/VERSION.major b/VERSION.major new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/VERSION.major @@ -0,0 +1 @@ +4 diff --git a/bin/polly b/bin/polly index d2672dc..08086ea 100755 --- a/bin/polly +++ b/bin/polly @@ -9,6 +9,18 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'polly' class PollyTasks < Thor + desc "static [PUBDIR]", "serves static files over http" + def static(pubdir = "public") + app = Rack::Builder.new do + map "/" do + use Rack::Static, :urls => [""], :root => 'public', :index => 'index.html', :header_rules => [[:all, {'cache-control' => 'private,max-age=0,must-revalidate,no-store'}]] + run proc { |env| } + end + end + + Rackup::Server.start(app: app) + end + desc "changelog [CHANGELOG]", "appends changelog item to CHANGELOG.md" def changelog(changelog = "CHANGELOG.md") exe = Polly::Execute.new @@ -23,7 +35,7 @@ class PollyTasks < Thor today = Date.today.to_s username = IO.popen("git config user.name").read.strip || ENV["USER"] || "ac" template_args = [today, username] - version = "2.#{version_count + 1}.0" + version = "#{File.exists?('VERSION.major') ? File.read('VERSION.major').strip : 1}.#{version_count + 1}.0" opening_line_template = "# [#{version}] - %s - %s\n\n\n\n#{version_delim}\n" % template_args Tempfile.create(changelog) do |new_entry_tmp| @@ -108,15 +120,15 @@ class PollyTasks < Thor end end - if options["u"] === true - if exe.systemx("git", "push", "-u", "origin", "HEAD") + if options["u"] + if exe.systemx("git", "push", "-u", options["u"], "HEAD") exit(0) end end if msg = options["m"] if exe.systemx("git", "add", ".") - if exe.systemx("git", "commit", "-m", (msg.length > 1) ? msg : "wip") + if exe.systemx(*["git", "commit", "-m", (msg.length > 1) ? msg : "wip", (msg.length > 1) ? nil : "--allow-empty"].compact) exit(0) end end @@ -141,14 +153,14 @@ class PollyTasks < Thor puts cmd_list if options["exec"] - exec(*cmd_list) + exec("bash", "-ex", "-c", cmd_list) end end end desc "test [PIPELINE]", "Run the CI pipeline" option "concurrency", :type => :numeric, :default => Polly::Plan::DEFAULT_CONCURRENCY, :desc => "Number of divergent paths to process simultaneously" - option "config", :type => :string, :default => "Pollyfile", :desc => "Declaration of config" + #option "config", :type => :string, :default => "Pollyfile", :desc => "Declaration of config" option "with-bootstrap", :type => :string, :default => nil, :desc => "Docker image to use as the build context" option "dry-run", :type => :boolean, :default => false, :desc => "Emit the plan but do not run the pipeline" option "keep-completed", :type => :boolean, :default => false, :desc => "Retain completed pods in kubectl get pods list" @@ -159,7 +171,9 @@ class PollyTasks < Thor option "only", :type => :string, :default => nil, :desc => "Only run specified jobs (comma-separated list)" option "no-init", :type => :boolean, :default => false, :desc => "Do not run pod initialization (eg git-checkout container) before each job (this can speed up debug looping)" option "ident", :type => :string, :default => nil, :desc => "todo" - def test(run_pipeline_upto_these_jobs = nil) + def test(config = nil) + default_config = "Pollyfile" + run_pipeline_upto_these_jobs = nil start_time = Time.now exe = ::Polly::Execute.new(options) @@ -171,23 +185,22 @@ class PollyTasks < Thor obv.register_channels(["debug", "plan", "exe"]) - config = options["config"] + #config = options["config"] - #TODO: switch plan based on detected hueristics here - if File.exists?(config) - #obv.report_stdout("debug", "using #{config} for config") - if config.include?("Pollyfile") - pollyfile_ruby = File.read(config) - Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, config) - generated_circleci_config_yml = YAML.dump(Polly::Generate.read_circleci_output(options["ident"])) + ##TODO: switch plan based on detected hueristics here + detected_config = config || default_config + if File.exists?(detected_config) + if detected_config == default_config + pollyfile_ruby = File.read(detected_config) + Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, detected_config) + exe_image = "polly-registry:23443/polly-registry/#{exe.current_app}:latest" + generated_circleci_config_yml = YAML.dump(Polly::Generate.read_circleci_output(exe_image, options["ident"])) plan.load_circleci(generated_circleci_config_yml) else - plan.load_circleci(File.read(config)) + plan.load_circleci(File.read(detected_config)) end - elsif File.exists?(".circleci/config.yml") - plan.load_circleci(File.read(".circleci/config.yml")) else - raise "must create #{config} first ..." + raise "must create #{detected_config} first ..." end obv.register_channels(plan.all_jobs.collect { |key, _| key }) @@ -201,7 +214,7 @@ class PollyTasks < Thor plan.jobs_ready_to_start.each do |job_proc| job_to_start = job_proc.call - + obv.report_stdout("plan", "started #{job_to_start.run_name}") obv.flush($stdout, $stderr) exe.start_job!(job_to_start) @@ -299,6 +312,7 @@ class PollyTasks < Thor #TODO: specify the remote follow branch with a toggle possibly??? exe.systemx("git", "push", "-f", "polly", branch, "HEAD:#{branch}-#{Time.now.to_i}", "--exec=polly receive-pack#{options['local'] ? " --local" : ""}") + #exe.systemx("git", "push", "-f", "polly", branch, "HEAD:#{branch}", "--exec=polly receive-pack#{options['local'] ? " --local" : ""}") #TODO: #####cat /var/tmp/polly-safe/buildkit/example-510b1e33549a9f97b9fce43f1a30d13afe208390.tar | docker import - example:latest @@ -334,9 +348,33 @@ class PollyTasks < Thor def receive_pack(origin = nil) exe = ::Polly::Execute.new + git_config_cmd = [ + "git", "config", "--global", "--add", "safe.directory", "/home/app/polly" + ] + + unless options["local"] + git_config_cmd = [ + "kubectl", "exec", exe.polly_pod, + "-i", + "--", + *git_config_cmd + ] + end + + #puts git_init_cmd + #exit + + o,e,s = exe.execute_simple(:output, git_config_cmd, {}) + unless s + puts o + puts e + exit(1) + end + exe.wait_child + git_init_cmd = [ "git", "init", "--bare", "/polly/safe/git/#{exe.current_app}", - "--template=/polly/app/config/git-repo/templates" + "--template=/home/app/config/git-repo/templates" ] unless options["local"] @@ -348,9 +386,12 @@ class PollyTasks < Thor ] end + #puts git_init_cmd + #exit + o,e,s = exe.execute_simple(:output, git_init_cmd, {}) - puts o unless s + puts o puts e exit(1) end @@ -404,110 +445,197 @@ class PollyTasks < Thor #end end - desc "docker-config", "installs docker-config-json secret" - def docker_config - exe = ::Polly::Execute.new - - docker_config_secret = [] - docker_config_secret << <<-HEREDOC ---- -apiVersion: v1 -kind: Secret -metadata: - name: docker-config -type: Opaque -data: - config.json: #{Base64.strict_encode64($stdin.read)} -... -HEREDOC - - apply_job = ["kubectl", "apply", "-f", "-"] - options = {:stdin_data => docker_config_secret.join} - o,e,s = exe.execute_simple(:output, apply_job, options) - puts [o, e] - end - desc "build [container_def]", "Build the current working directory's Dockerfile" #? option "run", :type => :string, :default => nil #? option "push", :type => :string, :default => nil - #option "docker-config-json", :type => :string, :default => nil option "cache", :type => :boolean, :default => true option "explain", :type => :boolean, :default => false option "in-cluster", :type => :boolean, :default => false - def build(container_definition="-") + option "stage", :type => :string, :default => nil + option "push", :type => :string, :default => nil + option "extra-tag", :type => :string, :default => nil + def build(container_definition=nil) default_pollyfile = "Pollyfile" default_dockerfile = "Dockerfile" exe = ::Polly::Execute.new(options) version = exe.current_revision - branch = exe.current_branch + branch = exe.current_branch.gsub("/", "-") app = exe.current_app image_repo = Polly::Config.image_repo generated_dockerfile = nil - if container_definition == "-" && File.exists?(default_pollyfile) + if (container_definition == nil && File.exist?(default_pollyfile)) || (container_definition && container_definition.include?(default_pollyfile) && File.exist?(container_definition)) pollyfile_ruby = File.read(default_pollyfile) #TODO: ??? Polly::Generate.options = options Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, default_pollyfile) generated_dockerfile = Polly::Generate.read_output - elsif container_definition == "-" && File.exists?(default_dockerfile) - generated_dockerfile = File.read(default_dockerfile) - else - generated_dockerfile = File.read(container_definition) - sub_version = container_definition.split(".").last - if sub_version != container_definition - app += "-" + sub_version - end - end - if options["in-cluster"] - Polly::Generate.all_images.each { |build_image| - puts app.inspect - Polly::Build.buildkit_internal(exe, app, build_image, version, generated_dockerfile, options["no-cache"]) - } - else - #build_dockerfile = [ - # {"DOCKER_BUILDKIT" => "1", "SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}, - # "docker", "build", "--progress=plain", "--ssh", "default", - # options["no-cache"] ? "--no-cache" : nil, "-t", - # app + ":" + version, ".", "-f", "-", generated_dockerfile.nil? ? {:in => File.open(container_definition) } : {:in => generated_dockerfile} - #].compact - - #build_dockerfile = [ - # {"DOCKER_BUILDKIT" => "1", "SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}, - # "docker", "build", "--progress=plain", "--ssh", "default", - # options["no-cache"] ? "--no-cache" : nil, "-t", - # app + ":" + version, ".", "-f", "-", generated_string_fd.nil? ? {:in => File.open(container_definition) } : {:in => generated_string_fd} - #].compact - - #Polly::Generate.all_images.each { |build_image| - # Polly::Build.buildkit_external(exe, app, build_image.stage, version, generated_dockerfile, options["no-cache"]) - #} + file = Tempfile.new('Dockerfile-pollygen-', Dir.pwd) + file_path = file.path + file.unlink + + File.write(file_path, generated_dockerfile) + puts file_path + #tag = build_image_to_tag(app, build_image_stage, version) + #buildctl_local_cmd = [ + # {"SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}, + # "buildctl", + # "--addr", "kube-pod://polly-buildkitd-0", + # "build", + # "--ssh", "default", #"default=#{Dir.home}/.ssh/id_rsa", + # "--frontend", "dockerfile.v0", + # "--local", "context=.", "--local", "dockerfile=.", #"--opt", "filename=#{File.basename(file.path)}", + + raise if version.empty? + Polly::Build.buildkit_workstation_to_controller(exe, app, version, branch, File.basename(file_path), options["stage"], options["no-cache"], options["push"], options["extra-tag"]) + exit + elsif (container_definition == nil && File.exist?(default_dockerfile)) || (container_definition && container_definition.include?(default_dockerfile) && File.exist?(container_definition)) raise if version.empty? + Polly::Build.buildkit_workstation_to_controller(exe, app, version, branch, container_definition || default_dockerfile, options["stage"], options["no-cache"], options["push"], options["extra-tag"]) + exit + + ##elsif container_definition == nil && File.exists?(default_dockerfile) + ## generated_dockerfile = File.read(default_dockerfile) + #else + # generated_dockerfile = File.read(container_definition) + end + + #elsif ((container_definition == nil && File.exists?(default_dockerfile)) + # raise if version.empty? + # Polly::Build.buildkit_workstation_to_controller(exe, app, version, branch, default_dockerfile, options["stage"], options["no-cache"], options["push"]) + # exit + + #elsif File.exists?(container_definition) + #container_definition == default_dockerfile) && + # #generated_dockerfile = File.read(default_dockerfile) + # raise if version.empty? + # Polly::Build.buildkit_workstation_to_controller(exe, app, version, branch, container_definition, options["stage"], options["no-cache"], options["push"]) + # exit + + ##elsif container_definition == nil && File.exists?(default_dockerfile) + ## generated_dockerfile = File.read(default_dockerfile) + #else + # generated_dockerfile = File.read(container_definition) + #end + + #if options["in-cluster"] + # Polly::Generate.all_images.each { |build_image| + # Polly::Build.buildkit_internal(exe, app, build_image, version, generated_dockerfile, options["no-cache"]) + # } + #else + # raise if version.empty? + # Polly::Build.buildkit_external(exe, app, branch, version, generated_dockerfile, options["no-cache"]) + #end - Polly::Build.buildkit_external(exe, app, "wkndr", version, generated_dockerfile, options["no-cache"]) + ##TODO: ? + #raise if version.empty? + #Polly::Build.buildkit_workstation_to_controller(exe, app, version, branch, "wkndr-todo", generated_dockerfile, options["no-cache"]) + + #exit + + #if container_definition == "-" && File.exists?(default_pollyfile) + # pollyfile_ruby = File.read(default_pollyfile) + # #TODO: ??? Polly::Generate.options = options + # Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, default_pollyfile) + # generated_dockerfile = Polly::Generate.read_output + #elsif container_definition == "-" && File.exists?(default_dockerfile) + # #generated_dockerfile = File.read(default_dockerfile) + #else + # generated_dockerfile = File.read(container_definition) + # sub_version = container_definition.split(".").last + # if sub_version != container_definition + # app += "-" + sub_version + # end + #end + + ##if options["in-cluster"] + ## Polly::Generate.all_images.each { |build_image| + ## Polly::Build.buildkit_internal(exe, app, build_image, version, generated_dockerfile, options["no-cache"]) + ## } + ##else + ## raise if version.empty? + ## Polly::Build.buildkit_external(exe, app, "wkndr", version, generated_dockerfile, options["no-cache"]) + ##end + end + + desc "deploy", "deploys kubernetes resources" + option "debug", :type => :boolean, :default => false + option "re-init", :type => :boolean, :default => false + option "with-registry", :type => :boolean, :default => false + option "with-bootstrap", :type => :string, :default => "ghcr.io/unhookd/polly:4.0-rc-3" + option "stage", :type => :string, :default => nil + def deploy(glob = "kubernetes/**/*yaml") + exe = ::Polly::Execute.new(options) + + version = exe.current_revision + branch = exe.current_branch.gsub("/", "-") + app = exe.current_app + image_repo = Polly::Config.image_repo - tag_dockerfile = ["docker", "tag", app + ":wkndr-" + version, app + ":latest"] - exe.systemx(*tag_dockerfile) + tag = Polly::Build.build_image_to_tag(app, version, options["stage"]) - tag_dockerfile = ["docker", "tag", app + ":wkndr-" + version, app + ":git-latest"] - exe.systemx(*tag_dockerfile) + resource_paths = Dir.glob(glob) - tag_dockerfile = ["docker", "tag", app + ":wkndr-" + version, "localhost/" + app + ":git-latest"] - exe.systemx(*tag_dockerfile) + all_resources = [] + + document_handler_switch = Proc.new do |document| + add_to_pending_documents = false + + description = document.to_ruby + + next unless description + + kind = description["kind"] + name = description["metadata"]["name"] + + case kind + when "Service" + + when "Deployment", "ReplicationController" + description["metadata"]["labels"].merge!(exe.polly_labels) + description["spec"]["template"]["metadata"]["labels"].merge!(exe.polly_labels) + if description["spec"]["template"]["spec"]["initContainers"] + description["spec"]["template"]["spec"]["initContainers"].each { |c| + if c["image"] == app + ":latest" + c["image"] = (image_repo + "/polly-registry/" + tag) #(app + ":" + branch + "-" + version)) + end + } + end + description["spec"]["template"]["spec"]["containers"].each { |c| + if c["image"] == app + ":latest" + c["image"] = (image_repo + "/polly-registry/" + tag) #(app + ":" + branch + "-" + version)) + end + } + + else + end + + all_resources << description.to_yaml end - #if options["push"] - # tag_dockerfile = ["docker", "tag", app + ":" + version, options["push"] + "/" + app + ":" + version] - # puts tag_dockerfile - # exe.systemx(*tag_dockerfile) + handler = Polly::DocumentStreamHandler.new(&document_handler_switch) + parser = Psych::Parser.new(handler) + resource_paths.each { |resource_path| + parser.parse(File.read(resource_path), resource_path) + } + + all_resources_yaml = all_resources.join("...\n") - # push_dockerfile = ["docker", "push", options["push"] + "/" + app + ":" + version] - # exe.systemx(*push_dockerfile) - #end + if options["debug"] + puts all_resources_yaml + exit + end + + apply_yaml = ["kubectl", "apply", "-f", "-"] + create_options = {:stdin_data => all_resources_yaml} + o,e,s = exe.execute_simple(:output, apply_yaml, create_options) + puts o + puts e + fail unless s end # desc "checkout [REPO] [VERSION] [DESTINATION]", "" @@ -567,22 +695,40 @@ HEREDOC # obv.flush($stdout, $stderr, true) # end - desc "certificates [PRIVATE_KEY] [PUBLIC_KEY]", "installs some common ca certificate secret material into cluster" + desc "certificates [CNAME]", "installs some common ca certificate secret material into cluster" option "namespace", :type => :string, :default => "default" option "mode", :type => :string, :default => "directory" - def certificates(inbound_ssh_key, inbound_ssh_key_pub, cname = "polly-registry", cname_two = "0.0.0.0") - inbound_ssh_key = File.read(inbound_ssh_key) - inbound_ssh_key_pub = File.read(inbound_ssh_key_pub) +# def certificates(inbound_ssh_key="", inbound_ssh_key_pub="", cname = "polly-registry", cname_two = "0.0.0.0") +# # openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 +# inbound_ssh_key = File.read(inbound_ssh_key) unless inbound_ssh_key.empty? +# inbound_ssh_key_pub = File.read(inbound_ssh_key_pub) unless inbound_ssh_key_pub.empty? + def certificates(cnames = "polly.local") + #inbound_ssh_key = File.read(inbound_ssh_key) unless inbound_ssh_key.empty? + #inbound_ssh_key_pub = File.read(inbound_ssh_key_pub) unless inbound_ssh_key_pub.empty? + #TODO: openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 + # + key_client = OpenSSL::PKey::RSA.new 4096 - key = OpenSSL::PKey::RSA.new 2048 - key_dsa = OpenSSL::PKey::DSA.new 2048 + key = OpenSSL::PKey::RSA.new 4096 + key_dsa = OpenSSL::PKey::DSA.new 4096 + + #TODO + #ssh_key = StringIO.new + #ssh_key.write(key.ssh_type) + #ssh_key.write([key.to_blob.length].pack('N')) + #ssh_key.write(key.to_blob) + #ssh_priv_key = ssh_key.string + + type = key_client.ssh_type + data2 = [ key_client.to_blob ].pack('m0') + openssh_format = "#{type} #{data2}" data = key.to_pem data_dsa = key_dsa.to_pem exe = ::Polly::Execute.new - root_key = OpenSSL::PKey::RSA.new 2048 # the CA's public/private key + root_key = OpenSSL::PKey::RSA.new 2048 # the CA's public/private key #!!!!!! root_ca = OpenSSL::X509::Certificate.new root_ca.version = 2 # cf. RFC 5280 - to make it a "v3" certificate @@ -602,6 +748,9 @@ HEREDOC root_ca.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false)) root_ca.sign(root_key, OpenSSL::Digest::SHA256.new) + cnames = cnames.split(":") + ["polly-registry"] + cname = cnames.first + key = OpenSSL::PKey::RSA.new 2048 cert = OpenSSL::X509::Certificate.new cert.version = 2 @@ -616,9 +765,41 @@ HEREDOC ef.issuer_certificate = root_ca cert.add_extension(ef.create_extension("keyUsage", "digitalSignature", true)) cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) - cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{cname},IP:#{cname_two}", false)) + cert.add_extension(ef.create_extension("subjectAltName", cnames.collect { |cn| "DNS:#{cn}" }.join(",") + ",IP:0.0.0.0", false)) cert.sign(root_key, OpenSSL::Digest::SHA256.new) + key2 = OpenSSL::PKey::RSA.new 2048 + cert2 = OpenSSL::X509::Certificate.new + cert2.version = 2 + cert2.serial = 2 + cert2.subject = OpenSSL::X509::Name.parse "/DC=org/DC=unhookd/CN=polly-buildkitd" + cert2.issuer = root_ca.subject # root CA is the issuer + cert2.public_key = key2.public_key + cert2.not_before = Time.now + cert2.not_after = cert2.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = cert2 + ef.issuer_certificate = root_ca + cert2.add_extension(ef.create_extension("keyUsage", "digitalSignature", true)) + cert2.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) + cert2.add_extension(ef.create_extension("subjectAltName", "DNS:polly-buildkitd,IP:127.0.0.1", false)) + cert2.sign(root_key, OpenSSL::Digest::SHA256.new) + + key3 = OpenSSL::PKey::RSA.new 2048 + cert3 = OpenSSL::X509::Certificate.new + cert3.version = 2 + cert3.serial = 2 + cert3.subject = OpenSSL::X509::Name.parse "/DC=org/DC=unhookd/CN=polly-buildctl" + cert3.issuer = root_ca.subject # root CA is the issuer + cert3.public_key = key3.public_key + cert3.not_before = Time.now + cert3.not_after = cert3.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = cert3 + ef.issuer_certificate = root_ca + cert3.add_extension(ef.create_extension("keyUsage", "digitalSignature", true)) + cert3.sign(root_key, OpenSSL::Digest::SHA256.new) + #key = File.read("/usr/local/etc/openssl/misc/workstation-key.pem") #cert = File.read("/usr/local/etc/openssl/misc/workstation-cert.pem") @@ -639,14 +820,13 @@ HEREDOC #kn.add("wkstation-service", key) #known_hosts = File.read("/var/tmp/cheese.known_hosts") #puts known_hosts.inspect + #TODO: !!! + #ssh_key: #{Base64.strict_encode64(inbound_ssh_key || "")} + #ssh_key_pub: #{Base64.strict_encode64(inbound_ssh_key_pub || "")} - #type = key.ssh_type - #data2 = [ key.to_blob ].pack('m0') - #openssh_format = "#{type} #{data2}" - ##puts openssh_format case options["mode"] - when "k8s-secret" + when "k8s-secrets" polly_certificates = <<-HEREDOC --- apiVersion: v1 @@ -659,30 +839,68 @@ data: dsa: #{Base64.strict_encode64(data_dsa)} dsa_pub: #{Base64.strict_encode64(key_dsa.public_key.to_s)} rsa_pub: #{Base64.strict_encode64(key.public_key.to_s)} - ssh_key: #{Base64.strict_encode64(inbound_ssh_key)} - ssh_key_pub: #{Base64.strict_encode64(inbound_ssh_key_pub)} -... ---- -apiVersion: v1 -kind: Secret -metadata: - name: "registry-certificates" -type: Opaque -data: - registry.polly.pem: #{Base64.strict_encode64(cert.to_pem)} - registry.polly.key: #{Base64.strict_encode64(key.to_pem)} -... ---- -apiVersion: v1 -kind: Secret -metadata: - name: "ca-certificates" -type: Opaque -data: - ca.workstation.crt: #{Base64.strict_encode64(a)} - ca.polly.crt: #{Base64.strict_encode64(b)} - ca-certificates.crt: #{Base64.strict_encode64(c)} ... +#--- +#apiVersion: v1 +#kind: Secret +#metadata: +# name: "registry-certificates" +#type: Opaque +#data: +# registry.polly.pem: #{Base64.strict_encode64(cert.to_pem)} +# registry.polly.key: #{Base64.strict_encode64(key.to_pem)} +# name: sshd +#type: Opaque +#data: +# rsa: #{Base64.strict_encode64(data)} +# dsa: #{Base64.strict_encode64(data_dsa)} +# dsa_pub: #{Base64.strict_encode64(key_dsa.public_key.to_s)} +# rsa_pub: #{Base64.strict_encode64(key.public_key.to_s)} +#... +##--- +##apiVersion: v1 +##kind: Secret +##metadata: +## name: "registry-certificates" +##type: Opaque +##data: +## registry.polly.pem: #{Base64.strict_encode64(cert.to_pem)} +## registry.polly.key: #{Base64.strict_encode64(key.to_pem)} +##... +##--- +##apiVersion: v1 +##kind: Secret +##metadata: +## name: "ca-certificates" +##type: Opaque +##data: +## ca.workstation.crt: #{Base64.strict_encode64(a)} +## ca.polly.crt: #{Base64.strict_encode64(b)} +## ca-certificates.crt: #{Base64.strict_encode64(c)} +##... +#--- +#apiVersion: v1 +#kind: Secret +#metadata: +# name: "buildkit-daemon-certs" +#type: Opaque +#data: +# ca.pem: #{Base64.strict_encode64(b)} +# cert.pem: #{Base64.strict_encode64(cert2.to_pem)} +# key.pem: #{Base64.strict_encode64(key2.to_pem)} +#... +#--- +#apiVersion: v1 +#kind: Secret +#metadata: +<<<<<<< HEAD +# name: "ca-certificates" +#type: Opaque +#data: +# ca.workstation.crt: #{Base64.strict_encode64(a)} +# ca.polly.crt: #{Base64.strict_encode64(b)} +# ca-certificates.crt: #{Base64.strict_encode64(c)} +#... HEREDOC obv = ::Polly::Observe.new @@ -702,29 +920,67 @@ HEREDOC obv.flush($stdout, $stderr, true) +# name: "buildkit-client-certs" +#type: Opaque +#data: +# ca.pem: #{Base64.strict_encode64(b)} +# cert.pem: #{Base64.strict_encode64(cert3.to_pem)} +# key.pem: #{Base64.strict_encode64(key3.to_pem)} +#... +#HEREDOC +# +# obv = ::Polly::Observe.new +# obv.register_channels(["certificates"]) +# +# delete_polly_certificates = ["kubectl", "delete", "--namespace=#{options['namespace']}", "-f", "-"] +# create_polly_certificates = ["kubectl", "create", "--namespace=#{options['namespace']}", "-f", "-"] +# +# options = {:stdin_data => polly_certificates} +# o,e,s = exe.execute_simple(:output, delete_polly_certificates, options) +# obv.report_io("certificates", o, e) +# obv.flush($stdout, $stderr) +# +# o,e,s = exe.execute_simple(:output, create_polly_certificates, options) +# obv.report_io("certificates", o, e) +# obv.flush($stdout, $stderr) +# +# obv.flush($stdout, $stderr, true) +# + when "directory" dir = File.join("/var/tmp", SecureRandom.uuid) FileUtils.mkdir_p(dir) - #TODO: fill in ssh bits - #File.write("#{dir}/sshd_rsa", data) + File.umask(0077) + + File.write("#{dir}/id_rsa.pub", openssh_format) + File.write("#{dir}/id_rsa", key_client.to_s) File.write("#{dir}/web-server-certificate.pem", cert) File.write("#{dir}/web-server-key.pem", key) + #TODO: sort out certificate-stack documentation + File.write("#{dir}/etc_slash_ssh_slash_ssh_host_rsa_key", key) + exe.ssh_private_key_from_rsa_private_key("#{dir}/etc_slash_ssh_slash_ssh_host_rsa_key") + File.write("#{dir}/etc_slash_ssh_slash_ssh_host_rsa_key.pub", exe.ssh_keygen_from_private_key("#{dir}/etc_slash_ssh_slash_ssh_host_rsa_key")) + File.write("#{dir}/from-workstation-ca.crt", a) File.write("#{dir}/polly-root-ca.crt", b) File.write("#{dir}/multipass-mega.crt", c) + File.write("#{dir}/buildkit-server-cert.crt", cert2) + File.write("#{dir}/buildkit-server-cert.key", key2) + + File.write("#{dir}/buildkit-client-cert.crt", cert3) + File.write("#{dir}/buildkit-client-cert.key", key3) + + #TODO: link with vertical.yaml specification puts "certificates will be found in #{dir}" end end - desc "init", "bootstraps project polly controller pod" - option "re-init", :type => :boolean, :default => false - option "with-registry", :type => :boolean, :default => false - option "with-bootstrap", :type => :string, :default => "ghcr.io/unhookd/polly:3.0-rc1" # "polly:3.0-rc1" - def init + desc "init [CERTS]", "bootstraps certificates into cluster" + def init(cert_package_dir) exe = ::Polly::Execute.new unless exe.in_polly? @@ -737,440 +993,368 @@ HEREDOC polly_deployments = [] polly_pvcs = [] - #polly_image = "#{exe.current_app}:#{exe.current_revision}" - #polly_command = ["polly", "dev", "/var/lib/polly/Procfile.init", "--rename=polly-init"].to_json - #apache2: /usr/sbin/apache2 -D FOREGROUND - #nginx-apt-proxy: /usr/sbin/nginx -g 'daemon off;' - - git_image = options["with-bootstrap"] # "alpine/git:latest" - git_command = ["sleep", "2147483647"].to_json - - polly_resources << <<-HEREDOC + #etc_slash_ssh_slash_ssh_host_rsa_key + #etc_slash_ssh_slash_ssh_host_rsa_key.pub + #from-workstation-ca.crt + #id_rsa.pub + #multipass-mega.crt + #polly-root-ca.crt + #web-server-certificate.pem + #web-server-key.pem + #File.write("#{dir}/web-server-certificate.pem", cert) + #File.write("#{dir}/web-server-key.pem", key) + #File.write("#{dir}/from-workstation-ca.crt", a) + #File.write("#{dir}/polly-root-ca.crt", b) + #File.write("#{dir}/multipass-mega.crt", c) + + a = File.open(File.join(cert_package_dir, "from-workstation-ca.crt")).read + b = File.open(File.join(cert_package_dir, "polly-root-ca.crt")).read + c = File.open(File.join(cert_package_dir, "multipass-mega.crt")).read + + cert = File.open(File.join(cert_package_dir, "web-server-certificate.pem")).read + key = File.open(File.join(cert_package_dir, "web-server-key.pem")).read + + key2 = File.open(File.join(cert_package_dir, "buildkit-server-cert.key")).read + cert2 = File.open(File.join(cert_package_dir, "buildkit-server-cert.crt")).read + key3 = File.open(File.join(cert_package_dir, "buildkit-client-cert.key")).read + cert3 = File.open(File.join(cert_package_dir, "buildkit-client-cert.crt")).read + + polly_certificates = <<-HEREDOC --- apiVersion: v1 -kind: ServiceAccount -metadata: - name: polly - namespace: default -... ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role +kind: Secret metadata: - name: polly - namespace: default -rules: -- apiGroups: ["*"] - resources: ["*"] - verbs: ["*"] + name: "registry-certificates" +type: Opaque +data: + registry.polly.pem: #{Base64.strict_encode64(cert)} + registry.polly.key: #{Base64.strict_encode64(key)} ... --- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding +apiVersion: v1 +kind: Secret metadata: - name: polly-bindings - namespace: default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: polly -subjects: -- kind: ServiceAccount - name: polly - namespace: default + name: "ca-certificates" +type: Opaque +data: + ca.workstation.crt: #{Base64.strict_encode64(a)} + ca.polly.crt: #{Base64.strict_encode64(b)} ... -HEREDOC - - polly_pvcs << <<-HEREDOC --- apiVersion: v1 -kind: PersistentVolumeClaim +kind: Secret metadata: - annotations: - labels: - stack: polly - name: polly-mount -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 10Gi - storageClassName: local-storage + name: "buildkit-daemon-certs" +type: Opaque +data: + ca.pem: #{Base64.strict_encode64(b)} + cert.pem: #{Base64.strict_encode64(cert2)} + key.pem: #{Base64.strict_encode64(key2)} ... --- apiVersion: v1 -kind: PersistentVolume +kind: Secret metadata: - name: polly-mount - labels: - stack: polly -spec: - capacity: - storage: 10Gi - accessModes: - - ReadWriteMany - storageClassName: local-storage - hostPath: - path: /var/tmp/polly-safe + name: "buildkit-client-certs" +type: Opaque +data: + ca.pem: #{Base64.strict_encode64(b)} + cert.pem: #{Base64.strict_encode64(cert3)} + key.pem: #{Base64.strict_encode64(key3)} ... HEREDOC -# polly_services << <<-HEREDOC -#--- -#apiVersion: v1 -#kind: Service -#metadata: -# name: "polly-headless" -#spec: -# clusterIP: "None" -# ports: -# - port: 8111 -# name: nginx-apt-proxy -# protocol: TCP -# - port: 8080 -# name: apache2 -# protocol: TCP -# - port: 5000 -# name: docker-registry -# protocol: TCP -# selector: -# name: "polly-app" -#... -#HEREDOC +#puts polly_certificates +#exit -# polly_services << <<-HEREDOC -#--- -#apiVersion: v1 -#kind: Service -#metadata: -# name: "polly-app" -#spec: -# ports: -# - port: 8111 -# name: nginx-apt-proxy -# protocol: TCP -# - port: 8080 -# name: apache2 -# protocol: TCP -# selector: -# name: "polly-app" -#... -#HEREDOC + # obv = ::Polly::Observe.new + # obv.register_channels(["certificates"]) - if options["with-registry"] - polly_services << <<-HEREDOC ---- -apiVersion: v1 -kind: Service -metadata: - name: "polly-registry" -spec: - ports: - - port: 443 - name: docker-registry - protocol: TCP - selector: - name: "polly-registry" -... -HEREDOC - end + # delete_polly_certificates = ["kubectl", "delete", "--namespace=#{options['namespace']}", "-f", "-"] + # create_polly_certificates = ["kubectl", "create", "--namespace=#{options['namespace']}", "-f", "-"] -# polly_services << <<-HEREDOC -#--- -#apiVersion: v1 -#kind: Service -#metadata: -# name: "polly-app-node" -#spec: -# type: NodePort -# ports: -# - port: 5000 -# nodePort: 31500 -# name: docker-registry-node -# protocol: TCP -# selector: -# name: "polly-app" -#... -#HEREDOC + # options = {:stdin_data => polly_certificates} + # o,e,s = exe.execute_simple(:output, delete_polly_certificates, options) + # obv.report_io("certificates", o, e) + # obv.flush($stdout, $stderr) - polly_deployments << <<-HEREDOC ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - labels: - app: buildkitd - name: polly-buildkitd -spec: - serviceName: buildkitd - podManagementPolicy: Parallel - replicas: 1 - selector: - matchLabels: - app: buildkitd - template: - metadata: - labels: - app: buildkitd - annotations: - container.apparmor.security.beta.kubernetes.io/buildkitd: unconfined - # see buildkit/docs/rootless.md for caveats of rootless mode - spec: - containers: - - name: buildkitd - image: moby/buildkit:master-rootless - args: - - --oci-worker-no-process-sandbox - readinessProbe: - exec: - command: - - buildctl - - debug - - workers - initialDelaySeconds: 5 - periodSeconds: 30 - livenessProbe: - exec: - command: - - buildctl - - debug - - workers - initialDelaySeconds: 5 - periodSeconds: 30 - securityContext: - # Needs Kubernetes >= 1.19 - seccompProfile: - type: Unconfined - # To change UID/GID, you need to rebuild the image - runAsUser: 1000 - runAsGroup: 1000 - volumeMounts: - # Dockerfile has `VOLUME /home/user/.local/share/buildkit` by default too, - # but the default VOLUME does not work with rootless on Google's Container-Optimized OS - # as it is mounted with `nosuid,nodev`. - # https://github.com/moby/buildkit/issues/879#issuecomment-1240347038 - - mountPath: /home/user/.local/share/buildkit - name: buildkitd - volumes: - - name: buildkitd - emptyDir: {} -... ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: polly-git - labels: - app: polly-git - stack: polly -spec: - revisionHistoryLimit: 1 - strategy: - type: Recreate - replicas: 1 - selector: - matchLabels: - name: polly-git - template: - metadata: - labels: - name: polly-git - stack: polly - spec: - serviceAccountName: polly - volumes: - - name: polly-mount - persistentVolumeClaim: - claimName: polly-mount - - name: registry-certificates - secret: - secretName: registry-certificates - securityContext: - runAsUser: 1000 - fsGroup: 1000 - #supplementalGroups: [121, 123, 134, 999, 1000, 1001] - initContainers: - containers: - - name: polly-git - securityContext: - runAsUser: 1000 - volumeMounts: - - mountPath: /polly/safe - name: polly-mount - - mountPath: /etc/ssl/private - name: registry-certificates - readOnly: true - image: #{git_image} - imagePullPolicy: IfNotPresent - env: - resources: - requests: - memory: 500Mi - cpu: 10m - limits: - memory: 8000Mi - cpu: 5000m - command: #{git_command} -... -HEREDOC + # o,e,s = exe.execute_simple(:output, create_polly_certificates, options) + # obv.report_io("certificates", o, e) + # obv.flush($stdout, $stderr) - if options["with-registry"] - polly_deployments << <<-HEREDOC ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: registry-config -data: - registry.conf: | - version: 0.1 - storage: - filesystem: - rootdirectory: /polly/safe/registry -... ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: polly-registry - labels: - app: polly-registry - stack: polly -spec: - revisionHistoryLimit: 1 - strategy: - type: Recreate - replicas: 1 - selector: - matchLabels: - name: polly-registry - template: - metadata: - labels: - name: polly-registry - stack: polly - spec: - volumes: - - name: registry-config - configMap: - name: registry-config - - name: polly-mount - persistentVolumeClaim: - claimName: polly-mount - - name: registry-certificates - secret: - secretName: registry-certificates - initContainers: - containers: - - name: polly-registry - volumeMounts: - - mountPath: /polly/safe - name: polly-mount - - mountPath: /etc/ssl/private - name: registry-certificates - readOnly: true - - mountPath: /registry.conf - subPath: registry.conf - name: registry-config - image: registry:2 - imagePullPolicy: IfNotPresent - resources: - requests: - memory: 500Mi - cpu: 500m - limits: - memory: 1000Mi - cpu: 2000m - ports: - - containerPort: 443 - command: - - /bin/registry - args: - - serve - - /registry.conf - env: - - name: REGISTRY_HTTP_TLS_CERTIFICATE - value: /etc/ssl/private/registry.polly.pem - - name: REGISTRY_HTTP_TLS_KEY - value: /etc/ssl/private/registry.polly.key - - name: REGISTRY_HTTP_ADDR - value: 0.0.0.0:443 -... -HEREDOC - end + # obv.flush($stdout, $stderr, true) - polly_run = polly_resources.join + polly_pvcs.join + polly_services.join + polly_deployments.join + polly_run = polly_certificates if options["re-init"] - deploy_polly_app = ["kubectl", "delete", "-f", "-"] + delete_polly_init = ["kubectl", "delete", "-f", "-"] options = {:stdin_data => polly_run} - o,e,s = exe.execute_simple(:output, deploy_polly_app, options) + o,e,s = exe.execute_simple(:output, deploy_polly_init, options) puts [o, e, s] - wait_polly_app_deleted = ["kubectl", "wait", "--for=delete", "pod", "-l", "stack=polly"] - o,e,s = exe.execute_simple(:output, wait_polly_app_deleted, {}) - puts [o, e, s] + #wait_polly_app_deleted = ["kubectl", "wait", "--for=delete", "pod", "-l", "stack=polly"] + #o,e,s = exe.execute_simple(:output, wait_polly_app_deleted, {}) + #puts [o, e, s] end - deploy_polly_app = ["kubectl", "apply", "-f", "-"] + deploy_polly_app = ["kubectl", "apply", "--server-side=true", "-f", "-"] options = {:stdin_data => polly_run} o,e,s = exe.execute_simple(:output, deploy_polly_app, options) puts [o, e, s] - wait_polly_app = ["kubectl", "wait", "--for=condition=available", "deployment/polly-git"] - o,e,s = exe.execute_simple(:output, wait_polly_app, {}) - puts [o, e, s] + #wait_polly_app = ["kubectl", "wait", "--for=condition=available", "deployment/polly-controller"] + #o,e,s = exe.execute_simple(:output, wait_polly_app, {}) + #puts [o, e, s] - wait_polly_app = ["kubectl", "rollout", "status", "deployment/polly-git"] - o,e,s = exe.execute_simple(:output, wait_polly_app, {}) - puts [o, e, s] + #wait_polly_app = ["kubectl", "rollout", "status", "deployment/polly-controller"] + #o,e,s = exe.execute_simple(:output, wait_polly_app, {}) + #puts [o, e, s] end desc "xxh", "debug shell into polly controller" - def xxh(sh_cmd = "bash") + def xxh(service = "controller", sh_cmd = "bash") exe = ::Polly::Execute.new - exec(*["kubectl", "exec", exe.polly_pod, "-i", $stdin.tty? ? "-t" : nil, "--", sh_cmd].compact) + exec(*["kubectl", "exec", exe.polly_pod(service), "-i", $stdin.tty? ? "-t" : nil, "--", sh_cmd].compact) end - desc "shh", "debug instance of polly controller" - def shh(sh_cmd = "bash") + desc "xxx", "debug shell into app" + def xxx(*sh_cmd) exe = ::Polly::Execute.new - exec(*["kubectl", "run", "-it", "--image=polly:latest", "--image-pull-policy=IfNotPresent", "--rm=true", "--quiet=true", "--", sh_cmd].compact) + exec(*["kubectl", "exec", exe.app_pod, "-i", $stdin.tty? ? "-t" : nil, "--", *sh_cmd].compact) end - desc "waitx", "TBD: wait for pod availability, execute subsequent command" - def waitx(clean_name, *cmd) + desc "shr", "run a polly shell" + def shr(shr_cmd = "bash") exe = ::Polly::Execute.new - #$stderr.write("waiting for deploy\n") - exe.execute_simple(:silent, ["kubectl", "wait", "--for=condition=available", "deployment/#{clean_name}"], {}) + exec(*["kubectl", "run", "shr", "-it", "--pod-running-timeout=3m0s", "--image=ghcr.io/unhookd/polly:master", "--image-pull-policy=IfNotPresent", "--rm=true", "--quiet=true", "--", shr_cmd].compact) + end - #$stderr.write("listing pods\n") - find_all_pods = "kubectl get pods -l name=#{clean_name} -o name | cut -d/ -f2" - a = IO.popen(find_all_pods).read.strip - exe.wait_child - all_pods = a.split("\n") + desc "rxn CMD", "rxn app pod" # uses --command override of ENTRYPOINT/CMD + option "stage", :type => :string, :default => nil + option "port", :type => :numeric, :default => nil + option "privileged", :type => :boolean, :default => false + def rxn(*cmd) + exe = ::Polly::Execute.new - pod_index = 0 - kube_exec_cmd = [ - "kubectl", "exec", - all_pods[pod_index], - "--" - ] + cmd + version = exe.current_revision + branch = exe.current_branch.gsub("/", "-") + app = exe.current_app + image_repo = Polly::Config.image_repo - o,e,s = exe.execute_simple(:output, kube_exec_cmd, {}) - #TODO - puts [e, e, s] unless s + tag = Polly::Build.build_image_to_tag(app, version, options["stage"]) + puts tag.inspect + + pod_name = "rxn-#{app}" + pod_image = "polly-registry:23443/polly-registry/#{tag}" + + cmd << "/bin/bash" if cmd.empty? + + spec_overrides = { + #"annotations" => { + # "container.apparmor.security.beta.kubernetes.io/#{pod_name}" => "unconfined", + # "container.seccomp.security.alpha.kubernetes.io/#{pod_name}" => "unconfined" + #}, + "spec" => { + "securityContext" => { + "appArmorProfile" => { + #"type" => "Unconfined" + "type" => "unconfined" + #"type" => "RuntimeDefault" + #"type" => "Localhost", + #"localhostProfile" => "Unconfined" + #"localhostProfile" => "Unconfined" + #"localhostProfile" => "k8s-apparmor-example-deny-write" + }, + #"fsGroup" => 1001 + }, + "containers" => [ + { + "name" => pod_name, + "image" => pod_image, + "imagePullPolicy" => "Always", + "command" => [ + *cmd + ], + "stdin" => true, + "stdinOnce" => true, + "tty" => true, + "privileged" => true, + "securityContext" => { + "seccompProfile" => { + "type" => "Unconfined" + #"type" => "unconfined" + }, + "appArmorProfile" => { + #"type" => "Unconfined" + "type" => "unconfined" + } + } + } + ] + } + } + + environment_overrides = nil + if File.exist?("vertical.yaml") + vertical_lookup = YAML.load(File.read("vertical.yaml")) + environment_overrides = vertical_lookup["environment-overrides"] + + service_account_name = vertical_lookup["service-account-name"] + + volume_mounts = vertical_lookup["volume-mounts"] + volumes = vertical_lookup["volumes"] + + envs = (environment_overrides || []) + + spec_overrides["spec"]["containers"][0]["env"] = envs + spec_overrides["spec"]["containers"][0]["volumeMounts"] = volume_mounts + spec_overrides["spec"]["volumes"] = volumes + spec_overrides["spec"]["serviceAccountName"] = service_account_name + end + + #"containers": [ + # { + # "name": "polly-bootstrap", + # "image": "alpine/git:latest", + # "command": ["/bin/sh"], + # "stdin": true, + # "stdinOnce": true, + # "tty": false, + # "volumeMounts": [{ + # "mountPath": "/polly-safe", + # "name": "polly-mount" + # }] + # } + #], + #"volumes": [{ + # "name":"polly-mount", + # "persistentVolumeClaim": { + # "claimName": "polly-mount" + # } + #}] + + expose_port_args = [] + if options["port"] + expose_port_args << "--expose=true" + expose_port_args << "--port=#{options['port']}" + end + + privileged_args = [] + if options["privileged"] + #privileged_args << "--privileged" + privileged_args << "--annotations=container.apparmor.security.beta.kubernetes.io/#{pod_name}=unconfined" + #privileged_args << "--annotations=container.seccomp.security.alpha.kubernetes.io/#{pod_name}=unconfined" + end + + cmd = ["kubectl", "run", pod_name, *privileged_args, "--attach=true", "-it", "--pod-running-timeout=3m0s", "--image=#{pod_image}", "--image-pull-policy=Always", "--rm=true", "--quiet=true", *expose_port_args, "--overrides", JSON.dump(spec_overrides), "--command", "--", *cmd].compact + puts cmd.inspect + + exec(*cmd) + end + + desc "exe", "exe app pod" # matches docker run use case, invokes ENTRYPOINT/CMD + option "stage", :type => :string, :default => nil + def exe + exe = ::Polly::Execute.new + + environment_overrides = nil + if File.exist?("vertical.yaml") + vertical_lookup = YAML.load(File.read("vertical.yaml")) + environment_overrides = vertical_lookup["environment-overrides"] + end + + env_flags = (environment_overrides || []).collect { |env| "--env=#{env["name"]}=#{env["value"]}" } + + version = exe.current_revision + branch = exe.current_branch.gsub("/", "-") + app = exe.current_app + image_repo = Polly::Config.image_repo + + tag = Polly::Build.build_image_to_tag(app, version, options["stage"]) + #tag = Polly::Build.build_image_to_tag(app, "wkndr", version) + puts tag.inspect + tag = "#{app}:latest" + + cmd = ["kubectl", "run", "exe-#{app}", "--attach=true", "--pod-running-timeout=3m0s", *env_flags, "--image=polly-registry:23443/polly-registry/#{tag}", "--image-pull-policy=Always", "--rm=true", "--quiet=true"].compact + # cmd = ["kubectl", "run", "rxn", "--attach=true", "-it", "--pod-running-timeout=3m0s", *env_flags, "--image=polly-registry:23443/polly-registry/#{tag}", "--image-pull-policy=Always", "--rm=true", "--quiet=true", "--command", "--", rxn_cmd].compact + puts cmd.inspect + + exec(*cmd) + end + + desc "wxh", "workstation instance" + def wxh(profile) + exe = ::Polly::Execute.new + + exec("ssh", "-AX", "app@#{exe.multipass_ip(profile)}") + end + + desc "waitx [CLEAN_NAME]", "wait for pod availability, execute subsequent command" + def waitx(clean_name = nil, *cmd) + puts "waiting for #{clean_name}" + + exe = ::Polly::Execute.new + + label_selector = clean_name || URI.encode_www_form(exe.polly_labels.to_a).gsub("&",",") + + waited_for_deployments = false + while !waited_for_deployments + #$stderr.write("waiting for deploy\n") + o,e,waited_for_deployments = exe.execute_simple(:output, ["kubectl", "wait", "--for=condition=available", "deployment", "-l", label_selector, "--timeout=30s"], {}) + puts o + puts e + end + + waited_for_pods = false + while !waited_for_pods + #$stderr.write("waiting for deploy\n") + o,e,waited_for_pods = exe.execute_simple(:output, ["kubectl", "wait", "--for=condition=ready", "pod", "-l", label_selector, "--timeout=30s"], {}) + puts o + puts e + end + + unless cmd.empty? + $stderr.write("listing pods to run #{cmd.inspect}\n") + find_all_pods = "kubectl get pods -l #{label_selector} -o name | cut -d/ -f2" + a = IO.popen(find_all_pods).read.strip + exe.wait_child + all_pods = a.split("\n") + + run_cmd_args = ["bash", "-e", "-o", "pipefail", "-c", cmd.join(" ")] + #"bash #{run_shell_path} > /proc/1/fd/1 2> /proc/1/fd/2"] + + pod_index = 0 + kube_exec_cmd = [ + "kubectl", "exec", + all_pods[pod_index], + "--" + ] + run_cmd_args + + #puts kube_exec_cmd.inspect #join(" ") + + s = exe.execute_simple(:async_wait_status, kube_exec_cmd, {}) #{:out => $stdout, :err => $stderr}) + + if s + #puts o + puts "OKOK" + else + #puts e + #fail + puts "fail" + fail + end + end end desc "logs", "fetch logs from polly controller pod" + option "polly", :type => :boolean, :default => false def logs exe = ::Polly::Execute.new - exec(*["kubectl", "logs", exe.polly_pod, "-f"].compact) + exec(*["kubectl", "logs", "-l", URI.encode_www_form(exe.polly_labels.to_a).gsub("&",","), "-f"].compact, "--max-log-requests=128") end desc "dev [PROCFILE]", "runs processes as outlined in Procfile" @@ -1201,6 +1385,8 @@ HEREDOC desc "generate", "emits CI structure yaml" option "mirror", :type => :string, :default => "dockerfile" + option "registry", :type => :string, :default => "polly-registry:23443/polly-registry" + #ghcr.io/unhookd #TODO: cleanup these bits #option "pipeline-fu", :type => :string, :default => "circleci" #option "image-fu", :type => :string, :default => "buildkit" @@ -1213,30 +1399,23 @@ HEREDOC Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, pollyfile) + #ghcr.io/unhookd/polly:3.0-rc1 + + exe_image = "#{options['registry']}/#{exe.current_app}:#{exe.current_branch}" + case options["mirror"] when "circleci" pollyfile_ruby = File.read("Pollyfile") Kernel.eval(pollyfile_ruby, Polly::Generate.get_binding, "Pollyfile") - generated_circleci_config_yml = YAML.dump(Polly::Generate.read_circleci_output) + generated_circleci_config_yml = YAML.dump(Polly::Generate.read_circleci_output(exe_image)) $stdout.write(generated_circleci_config_yml) when "github" - #set -e - #id app || (export DEBIAN_FRONTEND=noninteractive; export LC_ALL=C.UTF-8; export LANG=en_US; export LANGUAGE=en_US; export ACCEPT_EULA=y; apt-get update; apt-get install -y locales locales-all; apt-get clean; rm -rf /var/lib/apt/lists/*; locale-gen --purge en_US; /bin/echo -e "LANG=$LANG\nLANGUAGE=$LANGUAGE\n" | tee /etc/default/locale; locale-gen $LANGUAGE; dpkg-reconfigure locales; apt-get update; apt-get install -y git curl apt-transport-https aptitude ca-certificates apt-utils software-properties-common docker.io build-essential libyaml-dev ruby2* libruby2* ruby-bundler rubygems-integration rake amazon-ecr-credential-helper; apt-get clean; rm -rf /var/lib/apt/lists/*; curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add; apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"; apt-get update; apt-get install -y kubectl; apt-get clean; rm -rf /var/lib/apt/lists/*; groupadd --gid 134 abc-otf; groupadd --gid 999 efg-nnn; useradd --uid 1001 --home-dir /home/app --create-home --shell /bin/bash app --groups docker,abc-otf,efg-nnn; ln -sfv /home/app/polly/bin/polly /usr/local/bin/polly; chown -R app /home/app) - ####export SSH_AUTH_SOCK=/tmp/ssh-auth.sock - ####ssh-agent -a $SSH_AUTH_SOCK > /dev/null - #chown -R app . - #### /home/app - ####su app -s /bin/test -- -w $SSH_AUTH_SOCK || (chmod g+w $SSH_AUTH_SOCK && chgrp 1001 $SSH_AUTH_SOCK && chgrp 1001 $(dirname $SSH_AUTH_SOCK) && chmod g+x $(dirname $SSH_AUTH_SOCK) && su app -s /bin/test -- -w $SSH_AUTH_SOCK) - ####test -z $DOCKER_CERT_PATH || (chown -R 1001 $(dirname $DOCKER_CERT_PATH)) - ####chown root:docker /var/run/docker.sock - ####su app -w SSH_AUTH_SOCK,DOCKER_CERT_PATH,DOCKER_HOST,DOCKER_MACHINE_NAME,DOCKER_TLS_VERIFY,NO_PROXY -s /bin/bash -c 'bundle exec polly build' - #su app -s /bin/bash -c 'bundle config set --local path /home/app/vendor/bundle && bundle config set --local jobs 4 && bundle config set --local retry 3 && bundle config set --local deploment true && bundle config set --local without development && bundle install' - #su app -s /bin/bash -c 'bundle exec polly generate' > Dockerfile - ####docker run --rm=true polly:latest bundle exec rspec + #TODO: re-emit githubrc file when "dockerfile" $stdout.write(Polly::Generate.read_output) + else $stderr.puts "unknown option #{options.inspect}" end @@ -1468,7 +1647,7 @@ HEREDOC if options["build"] buildctl_local_cmd = [ "buildctl", - "--addr", "kube-pod://buildkitd-0?namespace=#{namespace}", + "--addr", "kube-pod://polly-buildkitd-0?namespace=#{namespace}", "build", "--ssh", "default=#{Dir.home}/.ssh/id_rsa", "--frontend", "dockerfile.v0", @@ -1498,45 +1677,83 @@ HEREDOC end end - #TODO: detect other repo categories to login different mech - desc "login [REPO]", "utility to authenticate to docker repo" - def login(repo_registry_url = nil) - exe = ::Polly::Execute.new - vertical_lookup = YAML.load(File.read("vertical.yaml")) - repo_registry = vertical_lookup["repo-registry"] - - docker_login_password = IO.popen("aws ecr get-login-password").read - Process.wait - unless $?.success? - exit(1) - end - - docker_login_cmd = ["docker", "login", "--username", "AWS", "--password-stdin", repo_registry] - options = {:stdin_data => docker_login_password} - o,e,s = exe.execute_simple(:output, docker_login_cmd, options) - puts [o, e, s] - end + ##TODO: re-impl without /usr/bin/docker + ##mkdir -p ~/.docker + ##kubectl create secret docker-registry --dry-run=client docker-regcred \ + ## --docker-server=XXX/repo \ + ## --docker-username=AWS \ + ## --docker-password=${PASSWORD} \ + ## --namespace=xxx \ + ## -o json | jq -r '.data.".dockerconfigjson"' | base64 -d > ~/.docker/config.json + #desc "login [REPO]", "utility to authenticate to docker repo" + #def login(repo_registry_url = nil) + # exe = ::Polly::Execute.new + # vertical_lookup = YAML.load(File.read("vertical.yaml")) + # repo_registry = vertical_lookup["repo-registry"] + + # docker_login_password = IO.popen("aws ecr get-login-password").read + # Process.wait + # unless $?.success? + # exit(1) + # end + + # docker_login_cmd = ["docker", "login", "--username", "AWS", "--password-stdin", repo_registry] + # options = {:stdin_data => docker_login_password} + # o,e,s = exe.execute_simple(:output, docker_login_cmd, options) + # puts [o, e, s] + #end desc "launch [PROFILE]", "todo" - option "platform", :type => :string, :default => "multipass", :desc => "todo" - option "distro", :type => :string, :default => "focal" - option "public-ssh-key", :type => :string, :default => "~/.ssh/id_rsa.pub", :desc => "todo" + option "debug", :default => false, :type => :boolean + option "automatic-host-key", :default => false, :type => :boolean def launch(profile = "foo") exe = ::Polly::Execute.new vertical_lookup = YAML.load(File.read("vertical.yaml")) - public_key = File.read(File.expand_path(options["public-ssh-key"])) + station_profile = vertical_lookup["platforms"][profile] + + station_profile.each { |platform, params| + case platform + when "multipass" + #TODO: pattern-matching ruby3x jazz + raise unless (distro = params["distro"]) && (ssh = params["ssh"]) && + (client_key_pub = ssh["client_key_pub"]) && + (server_key = ssh["server_key"]) && + (server_key_pub = ssh["server_key_pub"]) && + (ca_cert = ssh["ca_cert"]) + + cloud_init_yaml = ::Polly::Build.build_cloudinit_yaml(exe, vertical_lookup, File.read(ca_cert), File.read(client_key_pub), File.read(server_key), File.read(server_key_pub)) + + if options["debug"] + puts cloud_init_yaml + exit + end + + known_hosts_file = File.expand_path("~/.ssh/known_hosts") + known_hosts = File.read(known_hosts_file) if File.exists?(known_hosts_file) - cloud_init_yaml = ::Polly::Build.build_cloudinit_yaml(exe, vertical_lookup, public_key) + multipass_launch_cmd = ["multipass", "launch", "--name", profile, "--disk=100G", "--cpus=4", "--memory=10G", "--cloud-init=-", distro] + multipass_options = {:out => $stdout, :err => $stderr, :in => ::Polly::Build.generated_string_fd(cloud_init_yaml)} + exe.system({}, *multipass_launch_cmd, multipass_options) - multipass_launch_cmd = ["multipass", "launch", "--name", profile, "--disk=60G", "--cpus=4", "--mem=10G", "--cloud-init=-", options["distro"]] + server_key_pub_pem = File.read(server_key_pub) + known_hosts_line = "#{exe.multipass_ip(profile)} #{server_key_pub_pem}" + + if known_hosts && known_hosts.index(known_hosts_line) + else + if options["automatic-host-key"] || yes?("append #{known_hosts_file}?") + # echo $(ruby devstack/wkip.rb threep) $(ssh-keygen -yf tmp/threep/sshd_rsa_key) > ~/.ssh/known_hosts + File.write(known_hosts_file, (known_hosts || "") + "\n" + known_hosts_line + "\n") + end + end + + end + } - options = {:out => $stdout, :err => $stderr, :in => ::Polly::Build.generated_string_fd(cloud_init_yaml)} - exe.system({}, *multipass_launch_cmd, options) end desc "stationkeep [PROFILE]", "todo" - def stationkeep(profile = "foo") + def stationkeep(profile = "foo", substation = nil) exe = ::Polly::Execute.new vertical_lookup = YAML.load(File.read("vertical.yaml")) @@ -1544,45 +1761,55 @@ HEREDOC multipass_ip = exe.multipass_ip(profile) + if substation + stationkeeps = stationkeeps.select { |stationkeep| stationkeep["name"] == substation } + end + stationkeeps.each { |stationkeep| - if bootstrap = stationkeep["bootstrap"] - bootstrap_script = File.read(bootstrap) - shebang = bootstrap_script.split("\n")[0].split("!", 2)[1].split(" ") - - options = {:stdin_data => bootstrap_script} - ssh_pipe = ["ssh", "app@#{multipass_ip}", "sudo", *shebang] - stdin, stdout, stderr, wait_thr = Open3.popen3(*ssh_pipe) - stdin.write(options[:stdin_data]) - stdin.close - - resolution = :ok - begin - until wait_thr.join(1.0 / 24.0) - if bits = begin - stdout.readpartial(8192) - rescue EOFError - end - - $stdout.write(bits) - end - end - rescue Interrupt - Process.kill("INT", wait_thr.pid) - $stderr.write(stderr.read) - resolution = :interrupt - end + sudo = false + if bootstrap = stationkeep["root"] + sudo = true + elsif bootstrap = stationkeep["app"] + else + raise + end - $stdout.write(stdout.read) unless stdout.eof? - #$stderr.write(stderr.read) unless stderr.eof? + bootstrap_script = File.read(bootstrap) + shebang = bootstrap_script.split("\n")[0].split("!", 2)[1].split(" ") - unless wait_thr.value.success? - puts wait_thr.value.inspect - puts stderr.read - resolution = :errored + options = {:stdin_data => bootstrap_script} + ssh_pipe = ["ssh", "-AX", "app@#{multipass_ip}", sudo ? "sudo" : nil, *shebang].compact + stdin, stdout, stderr, wait_thr = Open3.popen3(*ssh_pipe) + stdin.write(options[:stdin_data]) + stdin.close + + resolution = :ok + begin + until wait_thr.join(1.0 / 24.0) + if bits = begin + stdout.readpartial(8192) + rescue EOFError + end + + $stdout.write(bits) + end end + rescue Interrupt + Process.kill("INT", wait_thr.pid) + $stderr.write(stderr.read) + resolution = :interrupt + end + + $stdout.write(stdout.read) unless stdout.eof? + #$stderr.write(stderr.read) unless stderr.eof? - break unless resolution == :ok + unless wait_thr.value.success? + puts wait_thr.value.inspect + puts stderr.read + resolution = :errored end + + break unless resolution == :ok } puts @@ -1590,7 +1817,7 @@ HEREDOC end desc "mkhooks [PROFILE]", "todo" - option "user", :type => :string, :default => "bip", :desc => "todo" + option "user", :type => :string, :default => "app", :desc => "todo" def mkhooks(profile = "foo") exe = ::Polly::Execute.new @@ -1609,18 +1836,43 @@ HEREDOC system("ssh", remote_user, "--", "git", "clone", "/home/#{options['user']}/workspace/#{uuid}.git", "/home/#{options['user']}/workspace/#{File.basename(Dir.pwd)}", "--branch", `git rev-parse --abbrev-ref HEAD`) || fail system("ssh", remote_user, "--", "ln", "-sf", "/home/#{options['user']}/workspace/#{File.basename(Dir.pwd)}/devstack/git-hooks", "/home/#{options['user']}/workspace/#{uuid}.git/hooks") system("ssh", remote_user, "--", "git", "-C", "/home/#{options['user']}/workspace/#{uuid}.git config", "receive.advertisePushOptions", "true") - #git config core.sshCommand 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ForwardAgent=yes -X' + + system("ssh", remote_user, "--", "git", "config", "--global", "user.name", IO.popen("git config user.name").read.strip) + system("ssh", remote_user, "--", "git", "config", "--global", "user.email", IO.popen("git config user.email").read.strip) + + polly_ssh_config_d = "#{Dir.home}/.ssh/polly_config.d" + FileUtils.mkdir_p(polly_ssh_config_d) + File.write(File.join(polly_ssh_config_d, profile), "Host #{multipass_ip}\n ForwardAgent yes") + + #system("git", "config", "remote.#{profile}.sshCommand", 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ForwardAgent=yes -X') else - puts "re-init manually... git remote remove #{foo}" + puts "re-init manually... git remote remove #{profile}" #system("ssh", remote_user, "--", "git", "-C", "/home/#{options['user']}/workspace/#{File.basename(Dir.pwd)}", "pull") #system("ssh", remote_user, "--", "git", "-C", "/home/#{options['user']}/workspace/#{File.basename(Dir.pwd)}", "show") #system("ssh", remote_user, "--", "git", "--git-dir", "/home/#{options['user']}/workspace/#{File.basename(Dir.pwd)}/.git", "pull") end end + desc "tags", "todo" + def tags + exe = ::Polly::Execute.new + polly_registry_ip = exe.polly_service("registry") + uri = URI("https://polly.local:23443/v2/polly-registry/polly/tags/list") + Net::HTTP.start(uri.host, uri.port, use_ssl: true, ipaddr: polly_registry_ip) do |http| + request = Net::HTTP::Get.new(uri) + response = http.request(request) + if response.code == "200" + parsed_response = JSON.parse(response.body) + puts parsed_response["tags"].sort + else + puts response.body + end + end + end + private - def fetch_from_registry(path) + def fetch_from_registry(path) #TODO: delete this? exe = ::Polly::Execute.new #registry_label = "name=polly-registry" diff --git a/config/git-repo/templates/hooks/post-update b/config/git-repo/templates/hooks/post-update index 023a3e4..0de7f44 100755 --- a/config/git-repo/templates/hooks/post-update +++ b/config/git-repo/templates/hooks/post-update @@ -22,10 +22,12 @@ then cd ${TMPCHECKOUT} unset GIT_DIR - GENERATED_CI=$(polly continuous) - if [ $? -eq 0 ]; then - ${GENERATED_CI} - fi + polly continuous --exec + + #GENERATED_CI=$(polly continuous) + #if [ $? -eq 0 ]; then + # ${GENERATED_CI} + #fi # bash -x -e -c "${GENERATED_CI}" & # wait diff --git a/kubernetes/resources.yaml b/kubernetes/resources.yaml new file mode 100644 index 0000000..6c0ba8d --- /dev/null +++ b/kubernetes/resources.yaml @@ -0,0 +1,403 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: polly + namespace: default +... +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: polly + namespace: default +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +... +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: polly-bindings + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: polly +subjects: +- kind: ServiceAccount + name: polly + namespace: default +... +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: polly +rules: +- apiGroups: + - "" + resources: + - nodes + - persistentvolumes + - clusterroles + - clusterrolebindings + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "rbac.authorization.k8s.io" + resources: + - clusterroles + - clusterrolebindings + verbs: + - get + - list + - watch + - create + - delete +... +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: polly-bindings +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: polly +subjects: +- kind: ServiceAccount + name: polly + namespace: default +... +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + labels: + stack: polly + name: polly-mount +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 50Gi + storageClassName: local-path +... +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: polly-mount + labels: + stack: polly +spec: + capacity: + storage: 50Gi + accessModes: + - ReadWriteMany + storageClassName: local-path + hostPath: + path: /var/tmp/polly-safe +... +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: buildkitd + name: polly-buildkitd +spec: + ports: + - port: 1234 + protocol: TCP + selector: + app: buildkitd +... +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: polly-buildkitd-configuration +data: + buildkitd.toml: |- + debug = true + [registry."polly-registry:23443"] + ca=["/certs/ca.polly.crt"] + insecure = true +... +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: buildkitd + stack: polly + name: polly-buildkitd +spec: + serviceName: polly-buildkitd + replicas: 1 + podManagementPolicy: Parallel + selector: + matchLabels: + app: buildkitd + template: + metadata: + labels: + name: polly-buildkitd + app: buildkitd + stack: polly + annotations: + container.apparmor.security.beta.kubernetes.io/buildkitd: unconfined + container.seccomp.security.alpha.kubernetes.io/buildkitd: unconfined + spec: + terminationGracePeriodSeconds: 180 + containers: + - name: buildkitd + image: moby/buildkit:v0.23.2-rootless + args: + - --oci-worker-no-process-sandbox + readinessProbe: + exec: + command: + - buildctl + - debug + - workers + initialDelaySeconds: 5 + periodSeconds: 30 + livenessProbe: + exec: + command: + - buildctl + - debug + - workers + initialDelaySeconds: 5 + periodSeconds: 30 + securityContext: + # Needs Kubernetes >= 1.19 + seccompProfile: + type: Unconfined + # To change UID/GID, you need to rebuild the image + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + # Dockerfile has `VOLUME /home/user/.local/share/buildkit` by default too, + # but the default VOLUME does not work with rootless on Google's Container-Optimized OS + # as it is mounted with `nosuid,nodev`. + # https://github.com/moby/buildkit/issues/879#issuecomment-1240347038 + - mountPath: /home/user/.local/share/buildkit + name: buildkitd + - name: configurations + subPath: buildkitd.toml + mountPath: /home/user/.config/buildkit/buildkitd.toml + - name: ca-certificates + mountPath: /certs + volumes: + - name: buildkitd + emptyDir: {} + - name: configurations + configMap: + name: polly-buildkitd-configuration + - name: ca-certificates + secret: + secretName: ca-certificates +... +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: polly-controller + labels: + app: polly-controller + stack: polly +spec: + revisionHistoryLimit: 1 + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + name: polly-controller + template: + metadata: + labels: + name: polly-controller + stack: polly + spec: + serviceAccountName: polly + volumes: + - name: buildkit-client-certs + secret: + secretName: buildkit-client-certs + - name: polly-mount + persistentVolumeClaim: + claimName: polly-mount + - name: registry-certificates + secret: + secretName: registry-certificates + securityContext: + runAsUser: 1000 + fsGroup: 1000 + #supplementalGroups: [121, 123, 134, 999, 1000, 1001] + initContainers: + containers: + - name: polly-controller + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - mountPath: /polly/safe + name: polly-mount + - mountPath: /etc/ssl/private + name: registry-certificates + readOnly: true + - mountPath: /certs/client + name: buildkit-client-certs + readOnly: true + image: polly:latest + imagePullPolicy: Always + env: + resources: + requests: + memory: 500Mi + cpu: 10m + limits: + memory: 8000Mi + cpu: 5000m + command: ["sleep", "infinity"] +... +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: registry-config +data: + registry.conf: | + version: 0.1 + storage: + filesystem: + rootdirectory: /polly/safe/registry +... +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: polly-registry + labels: + app: polly-registry + stack: polly +spec: + revisionHistoryLimit: 1 + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + name: polly-registry + template: + metadata: + labels: + name: polly-registry + stack: polly + spec: + hostNetwork: true + volumes: + - name: registry-config + configMap: + name: registry-config + - name: polly-mount + persistentVolumeClaim: + claimName: polly-mount + - name: registry-certificates + secret: + secretName: registry-certificates + initContainers: + containers: + - name: polly-registry + volumeMounts: + - mountPath: /polly/safe + name: polly-mount + - mountPath: /etc/ssl/private + name: registry-certificates + readOnly: true + - mountPath: /registry.conf + subPath: registry.conf + name: registry-config + image: registry:2 + imagePullPolicy: IfNotPresent + resources: + requests: + memory: 500Mi + cpu: 500m + limits: + memory: 1000Mi + cpu: 2000m + ports: + - containerPort: 23443 + name: https + command: + - /bin/registry + args: + - serve + - /registry.conf + env: + - name: REGISTRY_HTTP_TLS_CERTIFICATE + value: /etc/ssl/private/registry.polly.pem + - name: REGISTRY_HTTP_TLS_KEY + value: /etc/ssl/private/registry.polly.key + - name: REGISTRY_HTTP_ADDR + value: 0.0.0.0:23443 +... +--- +apiVersion: v1 +kind: Service +metadata: + name: "polly-registry" + labels: + name: "polly-registry" + annotations: + #traefik.ingress.kubernetes.io/service.serverstransport: default-polly-registry-transport@kubernetescrd +spec: + ports: + - port: 23443 + targetPort: 23443 + name: https + protocol: TCP + selector: + name: "polly-registry" +... +#TODO: should polly-registry be an Ingress???? security issues exposing polly-registry to wide internet? +#--- +#apiVersion: networking.k8s.io/v1 +#kind: Ingress +#metadata: +# name: polly-registry-vhost +#spec: +# rules: +# - host: polly-registry +# http: +# paths: +# - pathType: Prefix +# path: / +# backend: +# service: +# name: polly-registry +# port: +# name: https +#... +#--- +#apiVersion: traefik.containo.us/v1alpha1 +#kind: ServersTransport +#metadata: +# name: polly-registry-transport +#spec: +# insecureSkipVerify: true # Skip SSL verification +#... diff --git a/lib/polly.rb b/lib/polly.rb index 5e236b5..1ef0417 100644 --- a/lib/polly.rb +++ b/lib/polly.rb @@ -1,4 +1,4 @@ -if Dir.exists?(File.expand_path("../.bundle", __dir__)) +if Dir.exist?(File.expand_path("../.bundle", __dir__)) ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. @@ -19,6 +19,9 @@ require 'pathname' require 'fileutils' require 'net/ssh' +require 'rack' +require 'rackup' +require 'net/http' module Polly POLLY = "polly" @@ -32,4 +35,5 @@ class Error < StandardError; end autoload 'Job', 'polly/job' autoload 'Observe', 'polly/observe' autoload 'Plan', 'polly/plan' + autoload 'DocumentStreamHandler', 'polly/document_stream_handler' end diff --git a/lib/polly/build.rb b/lib/polly/build.rb index b4bc013..3f18fdf 100644 --- a/lib/polly/build.rb +++ b/lib/polly/build.rb @@ -2,9 +2,12 @@ module Polly class Build - - def self.build_image_to_tag(app, build_image_stage, version) - app + ":" + build_image_stage + "-" + version + def self.build_image_to_tag(app, version, build_image_stage=nil) + if build_image_stage + app + ":" + build_image_stage + "-" + version + else + app + ":" + version + end end def self.generated_string_fd(generated_dockerfile) @@ -14,30 +17,78 @@ def self.generated_string_fd(generated_dockerfile) fd end - def self.buildkit_external(exe, app, build_image_stage, version, generated_dockerfile, force_no_cache) - tag = build_image_to_tag(app, build_image_stage, version) - - build_dockerfile = [ - {"DOCKER_BUILDKIT" => "1", "SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}, - "docker", "build", "--progress=plain", "--ssh", "default", - force_no_cache ? "--no-cache" : nil, - #"--target", build_image_stage, - "-t", tag, - "-f", "-", - ".", - {:in => generated_string_fd(generated_dockerfile)} - ].compact - - #o,e,s = exe.execute_simple(:output, build_dockerfile, io_options) - #puts [o, e] - puts build_dockerfile.inspect - exe.systemx(*build_dockerfile) - + def self.buildkit_workstation_to_controller(exe, app, version, branch, dockerfile_path, build_image_stage, force_no_cache = nil, push_stage = nil, extra_tag = nil) + tag = build_image_to_tag(app, version, build_image_stage) + + buildctl_local_cmd = [ + {"SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}.compact, + "buildctl", + "--addr", "kube-pod://polly-buildkitd-0", + "build", + ####"debug", "dump-llb", + "--progress=plain", + *(ENV["SSH_AUTH_SOCK"] ? ["--ssh", "default"] : []), #"default=#{Dir.home}/.ssh/id_rsa", + "--frontend", "dockerfile.v0", + "--local", "context=.", "--local", "dockerfile=.", + "--opt", "filename=#{dockerfile_path}", + "--import-cache", + "type=registry,ref=polly-registry:23443/#{app}", + "--import-cache", + "type=local,src=/var/tmp/polly-safe/buildkit,mode=max", + "--export-cache", + "type=inline", + "--export-cache", + "type=registry,ref=polly-registry:23443/#{app}", + #"--export-cache", + #"type=local,dest=/var/tmp/polly-safe/buildkit,mode=max" # this is client-side + ] + + if build_image_stage + buildctl_local_cmd += ["--opt", "target=#{build_image_stage}"] + end + + #if push_stage + # buildctl_local_cmd += ["--output", "type=image,name=#{push_stage}/#{tag.split(':').last},push=true"] + #else + # buildctl_local_cmd += ["--output", "type=image,name=polly-registry:23443/polly-registry/#{tag},name=polly-registry:23443/polly-registry/#{app}:latest,push=true"] + # #buildctl_local_cmd += ["--output", "type=image,name=polly-registry:23443/polly-registry/#{app}:latest,push=true"] + #end + + if extra_tag + buildctl_local_cmd += ["--output", "type=image,name=#{extra_tag}"] + end + + puts buildctl_local_cmd.inspect + exe.systemx(*buildctl_local_cmd, "--output", "type=image,\"name=polly-registry:23443/polly-registry/#{tag},polly-registry:23443/polly-registry/#{app}:latest\",push=true") || fail("unable to build") + #exe.systemx(*buildctl_local_cmd, "--output", "type=image,name=polly-registry:23443/polly-registry/#{app}:latest,push=true") || fail("unable to build") puts "Built and tagged: #{tag} OK" end + def self.buildkit_external(exe, app, build_image_stage, version, generated_dockerfile, force_no_cache) + raise + ##file = Tempfile.new('Dockerfile', Dir.pwd) + ##file.write(generated_dockerfile) + ##file.rewind + ##puts file.path + #tag = build_image_to_tag(app, build_image_stage, version) + #buildctl_local_cmd = [ + # {"SSH_AUTH_SOCK" => ENV["SSH_AUTH_SOCK"]}, + # "buildctl", + # "--addr", "kube-pod://polly-buildkitd-0", + # "build", + # "--ssh", "default", #"default=#{Dir.home}/.ssh/id_rsa", + # "--frontend", "dockerfile.v0", + # "--local", "context=.", "--local", "dockerfile=.", #"--opt", "filename=#{File.basename(file.path)}", + # "--output", "type=image,name=polly-registry:443/polly-registry/#{tag},push=true" #, + # #{:in => generated_string_fd(generated_dockerfile)} + #] + #puts buildctl_local_cmd.inspect + #exe.systemx(*buildctl_local_cmd) + #puts "Built and tagged: #{tag} OK" + end + def self.buildkit_internal(exe, app, build_image_stage, version, generated_dockerfile, force_no_cache) - tag = build_image_to_tag(app, build_image_stage.stage, version) + tag = build_image_to_tag(app, version, build_image_stage.stage) stage = app + "-" + build_image_stage.stage polly_dockerfile_config = [] @@ -126,7 +177,7 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke args: - build #- --import-cache - #- type=registry,ref=polly-registry:443/#{app} + #- type=registry,ref=polly-registry:23443/#{app} - --import-cache - type=local,src=/polly/safe/buildkit,mode=max - --frontend @@ -138,7 +189,7 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke #- --export-cache #- type=inline #- --export-cache - #- type=registry,ref=polly-registry:443/#{app} + #- type=registry,ref=polly-registry:23443/#{app} - --export-cache - type=local,dest=/polly/safe/buildkit,mode=max #- --output @@ -146,7 +197,7 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke #- --output #- type=image,name=#{app}/#{tag},push=true ##- --output - ##- type=image,name=polly-registry:443/#{tag},push=true + ##- type=image,name=polly-registry:23443/#{tag},push=true resources: requests: memory: 5000Mi @@ -158,9 +209,6 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke runAsUser: 1000 runAsGroup: 1000 volumeMounts: - #- mountPath: /home/user/.docker/config.json - # subPath: config.json - # name: docker-config - mountPath: /tmp/#{app}/Dockerfile subPath: Dockerfile name: polly-dockerfile-#{app} @@ -177,9 +225,6 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke subPath: ca-certificates.crt name: ca-certificates volumes: - #- name: docker-config - # secret: - # secretName: docker-config - name: polly-dockerfile-#{app} configMap: name: polly-dockerfile-#{app} @@ -227,7 +272,7 @@ def self.buildkit_internal(exe, app, build_image_stage, version, generated_docke exec(*["kubectl", "logs", build_pod, "-f"].compact) end - def self.build_cloudinit_yaml(exe, vertical_lookup, public_ssh_key) + def self.build_cloudinit_yaml(exe, vertical_lookup, ca_cert, client_key_pub, server_key, server_key_pub) prewrites = vertical_lookup["prewrites"] users = [{ @@ -235,7 +280,7 @@ def self.build_cloudinit_yaml(exe, vertical_lookup, public_ssh_key) 'shell' => '/bin/bash', 'groups' => 'sudo', 'sudo' => 'ALL=(ALL) NOPASSWD:ALL', - 'ssh_authorized_keys' => [public_ssh_key] + 'ssh_authorized_keys' => [client_key_pub] }] write_files = [] @@ -250,9 +295,112 @@ def self.build_cloudinit_yaml(exe, vertical_lookup, public_ssh_key) } } + write_files << { + 'content' => server_key.strip + "\n", + 'path' => '/etc/ssh/custom_ssh_host_rsa_key', + 'permissions' => '0600' + } + + write_files << { + 'content' => server_key_pub.strip + " root@threep" + "\n", + 'path' => '/etc/ssh/custom_ssh_host_rsa_key.pub', + 'permissions' => '0600' + } + + write_files << { + 'content' => ca_cert.strip + "\n", + 'path' => '/usr/local/share/ca-certificates/polly-ca.crt', + 'permissions' => '0644' + } + + #write_files << { + # 'content' => "KUBECONFIG=\"~/.kube/k3s-config:~/.kube/config\"" + "\n" + "PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin\"" + "\n", + # 'path' => '/etc/environment', + # 'permissions' => '0644' + #} + + write_files << { + 'content' => "HostKey /etc/ssh/custom_ssh_host_rsa_key" + "\n", + 'path' => '/etc/ssh/sshd_config.d/custom.conf', + 'permissions' => '0644' + } + + write_files << { + 'path' => '/etc/systemd/resolved.conf', + 'append' => true, + 'content' => "MulticastDNS=yes\n" + } + + write_files << { + 'path' => '/etc/systemd/system/mdns@.service', + 'content' => "[Service] +Type=oneshot +ExecStart=/usr/bin/resolvectl mdns %i yes +After=sys-subsystem-net-devices-%i.device + +[Install] +WantedBy=sys-subsystem-net-devices-%i.device +" + } + + runcmd = [ + "systemctl restart systemd-resolved.service", + "systemctl start mdns@ens3.service", # https://github.com/canonical/multipass/issues/1830 + "systemctl enable mdns@ens3.service" + ] + +#mirrors: +# "docker.io": +# endpoint: +# - "https://polly-registry:443" +# "polly-registry": +# endpoint: +# - "https://polly-registry:23443" +#configs: +# "polly-registry:23443": +# tls: +# #cert_file: # path to the cert file used in the registry +# #key_file: # path to the key file used in the registry +# ca_file: /home/app/workspace/polly/ca # path to the ca file used in the registry + polly_registry_k3s_config = { + "mirrors" => { + #"docker.io" => { + # "endpoint" => [ + # "https://polly-registry:443" + # ] + #}, + "polly-registry" => { + "endpoint" => [ + "https://polly-registry:23443" + ] + } + }, + "configs" => { + "polly-registry:23443" => { + "tls" => { + "ca_file" => "/usr/local/share/ca-certificates/polly-ca.crt" + } + } + } + } + + write_files << { + 'content' => YAML.dump(polly_registry_k3s_config), + 'path' => '/etc/rancher/k3s/registries.yaml', + 'permissions' => '0644' + } + + write_files << { + 'content' => "127.0.0.1 polly-registry\n127.0.1.1 $hostname $hostname\n127.0.0.1 localhost\n" + vertical_lookup["host-aliases"].collect { |ha| ha["hostnames"].collect { |hn| ha["ip"] + " " + hn }.join("\n") }.join("\n") + "" + "\n", + 'path' => '/etc/cloud/templates/hosts.debian.tmpl', + 'permissions' => '0644' + } + { 'users' => users, - 'write_files' => write_files + 'write_files' => write_files, + 'manage_etc_hosts' => true, + 'runcmd' => runcmd }.to_yaml end end diff --git a/lib/polly/config.rb b/lib/polly/config.rb index a598ef7..2889509 100644 --- a/lib/polly/config.rb +++ b/lib/polly/config.rb @@ -11,11 +11,11 @@ def self.rc end def self.allowed_contexts - rc["allowed_contexts"] || ["polly-ci", "kubernetes-admin@kubernetes"] + rc["allowed_contexts"] || ["polly-ci", "kubernetes-admin@kubernetes", "default"] end def self.image_repo - rc["image_repo"] || "polly-registry:443" + rc["image_repo"] || "polly-registry:23443" end end end diff --git a/lib/polly/document_stream_handler.rb b/lib/polly/document_stream_handler.rb new file mode 100644 index 0000000..1fd75ea --- /dev/null +++ b/lib/polly/document_stream_handler.rb @@ -0,0 +1,18 @@ +module Polly + class DocumentStreamHandler < Psych::TreeBuilder + def initialize &block + super + @block = block + end + + def end_document implicit_end = !streaming? + @last.implicit_end = implicit_end + @block.call pop + end + + def start_document version, tag_directives, implicit + n = Psych::Nodes::Document.new version, tag_directives, implicit + push n + end + end +end diff --git a/lib/polly/execute.rb b/lib/polly/execute.rb index 205c9c1..220bb8f 100644 --- a/lib/polly/execute.rb +++ b/lib/polly/execute.rb @@ -51,7 +51,7 @@ def systemx(*cmd) cmd.unshift("echo") if @explain end - status = Kernel.system(*cmd) + status = Kernel.system(*cmd, {:err => $stderr}) unless status Kernel.exit(1) end @@ -59,7 +59,7 @@ def systemx(*cmd) end def check_current_kube_context_is_safe! - if File.exists?(File.join(Etc.getpwuid.dir, ".kube/config")) || ENV['KUBE_CONFIG'] + if File.exist?(File.join(Etc.getpwuid.dir, ".kube/config")) || ENV['KUBE_CONFIG'] begin current_kube_context = IO.popen("kubectl config current-context").read.strip return true if current_kube_context.empty? @@ -88,6 +88,10 @@ def current_app end end + def polly_labels + {"polly-current-app" => current_app} + end + def current_branch @current_branch ||= begin a = IO.popen("git rev-parse --abbrev-ref HEAD").read.strip @@ -126,6 +130,41 @@ def multipass_ip(profile) end end + def ssh_keygen_from_private_key(private_key) + keygen_cmd = [ + "ssh-keygen", + "-yf", + private_key + ] + + stdout_and_stderr_str, status = Open3.capture2e(*keygen_cmd) + unless status.success? + puts stdout_and_stderr_str + exit(1) + end + + stdout_and_stderr_str + end + + def ssh_private_key_from_rsa_private_key(private_key) + keygen_cmd = [ + "ssh-keygen", + "-p", + "-N", + "", + "-f", + private_key + ] + + stdout_and_stderr_str, status = Open3.capture2e(*keygen_cmd) + unless status.success? + puts stdout_and_stderr_str + exit(1) + end + + stdout_and_stderr_str + end + def start_job!(job) clean_name = (current_app + "-" + job.run_name).gsub(/[^\.a-z0-9]/, "-")[0..34] @@ -140,7 +179,7 @@ def start_job!(job) extra_runtime_envs = begin if executor_hints[:setup_remote_docker] || clean_name.include?("bootstrap") - {"SSH_AUTH_SOCK" => "/home/app/.ssh-auth-sock"} + {} #{"SSH_AUTH_SOCK" => "/home/app/.ssh-auth-sock"} else {} end @@ -159,17 +198,27 @@ def start_job!(job) Kernel.exit(1) end - docker_image_url = URI.parse("http://local/#{first_docker_executor_hint["image"]}") - repo = docker_image_url.host + first_docker_executor_hint["image"] + + #docker_image_url = URI.parse("http://local/#{first_docker_executor_hint["image"]}") + #repo = docker_image_url.host + ###TODO: ???? File.basename(docker_image_url.path) + #Pathname.new(docker_image_url.path).relative_path_from(Pathname.new("/")).to_s - #TODO: ???? File.basename(docker_image_url.path) - Pathname.new(docker_image_url.path).relative_path_from(Pathname.new("/")).to_s + ##add_circleci_job + ##buildctl_local_cmd += ["--output", "type=image,name=polly-registry:23443/polly-registry/#{tag},push=true"] + #version = current_revision + #branch = current_branch.gsub("/", "-") + #app = current_app + ##image_repo = Polly::Config.image_repo + #"polly-registry:23443/polly-registry/#{app}:#{version}" end end build_run_dir = Dir.mktmpdir #"/polly/safe/run" build_manifest_dir = File.join(build_run_dir, clean_name, current_revision) - run_shell_path = File.join(build_manifest_dir, "run.sh") + + #run_shell_path = File.join(build_manifest_dir, "run.sh") sleep_cmd_args = ["sleep", "infinity"] @@ -178,12 +227,16 @@ def start_job!(job) #####TODO: better input for cmd: [] support ######run_cmd_args = ["bash", "-e", run_shell_path] - FileUtils.mkdir_p(build_manifest_dir) - File.write(run_shell_path, job.parameters[:command]) + #FileUtils.mkdir_p(build_manifest_dir) + #File.write(run_shell_path, job.parameters[:command]) #run_cmd_args = ["bash", "-e", "-x", "-o", "pipefail", run_shell_path] #if true #TODO: bits - run_cmd_args = ["bash", "-e", "-o", "pipefail", "-c", "bash #{run_shell_path} > /proc/1/fd/1 2> /proc/1/fd/2"] + #run_cmd_args = ["bash", "-e", "-o", "pipefail", "-c", "bash #{run_shell_path} > /proc/1/fd/1 2> /proc/1/fd/2"] + run_cmd_args = ["/bin/bash /home/app/workflows/run.sh"] # > /proc/1/fd/1 2> /proc/1/fd/2"] + #puts run_shell_path + + #run_cmd_args = ["sleep infinity"] # > /proc/1/fd/1 2> /proc/1/fd/2"] #end intend_to_run_cmd = nil @@ -207,7 +260,8 @@ def start_job!(job) "metadata" => { "name" => clean_name, "labels" => { - "app" => clean_name + "app" => clean_name, + "polly" => "polly-ci" } }, "spec" => { @@ -219,15 +273,16 @@ def start_job!(job) "selector" => { "matchLabels" => { #TODO: abstract this!!!! - "name" => clean_name + "app" => clean_name, + "polly" => "polly-ci" } }, "template" => { "metadata" => { "labels" => { #TODO: abstract this - "name" => clean_name, - "app" => "polly-ci" + "app" => clean_name, + "polly" => "polly-ci" }, "annotations" => {} } @@ -236,8 +291,44 @@ def start_job!(job) } container_spec = { + "serviceAccount" => "polly", ##TODO: converge this with workstion git context "initContainers" => [ + { + #"terminationGracePeriodSeconds" => 5, + "name" => "git-config", + "image" => "alpine/git:latest", #TODO: more bits rebootstrap + "workingDir" => "/home/app/#{current_app}", #TODO: local executor support + "imagePullPolicy" => "IfNotPresent", + "args" => [ + #origin = "/polly-safe/git/#{app}" + #"http://polly-app:8080/#{current_app}" + #"clone", "-b", current_branch, "/polly/safe/git/#{current_app}", ".", + "config", "--global", "--add", "safe.directory", "/home/app/polly", + ], + "env" => { "GIT_CONFIG_GLOBAL" => "/home/app/.config/.gitconfig", "GIT_DISCOVERY_ACROSS_FILESYSTEM" => "true" }.collect { |k,v| {"name" => k, "value" => v } }, + "securityContext" => { + "runAsUser" => 1000, #TODO: ??username_to_uid("app"), #TODO: bootstrap module + "runAsGroup" => 1000, #TODO: ??username_to_uid("app"), #TODO: bootstrap module + "allowPrivilegeEscalation" => false, + "readOnlyRootFilesystem" => true + }, + "volumeMounts" => [ + { + "mountPath" => "/home/app/#{current_app}", + "name" => "scratch-dir" + }, + { + "mountPath" => "/home/app/.config", + "name" => "config-dir" + }, + { + "mountPath" => "/polly/safe/git/#{current_app}", + "name" => "git-repo" + }, + + ] + }, { #"terminationGracePeriodSeconds" => 5, "name" => "git-clone", @@ -249,9 +340,10 @@ def start_job!(job) #"http://polly-app:8080/#{current_app}" "clone", "-b", current_branch, "/polly/safe/git/#{current_app}", "." ], - "env" => { "GIT_DISCOVERY_ACROSS_FILESYSTEM" => "true" }.collect { |k,v| {"name" => k, "value" => v } }, + "env" => { "GIT_CONFIG_GLOBAL" => "/home/app/.config/.gitconfig", "GIT_DISCOVERY_ACROSS_FILESYSTEM" => "true" }.collect { |k,v| {"name" => k, "value" => v } }, "securityContext" => { - "runAsUser" => username_to_uid("root"), #TODO: bootstrap module + "runAsUser" => 1000, #TODO: ??username_to_uid("app"), #TODO: bootstrap module + "runAsGroup" => 1000, #TODO: ??username_to_uid("app"), #TODO: bootstrap module "allowPrivilegeEscalation" => false, "readOnlyRootFilesystem" => true }, @@ -260,6 +352,10 @@ def start_job!(job) "mountPath" => "/home/app/#{current_app}", "name" => "scratch-dir" }, + { + "mountPath" => "/home/app/.config", + "name" => "config-dir" + }, { "mountPath" => "/polly/safe/git/#{current_app}", "name" => "git-repo" @@ -287,7 +383,7 @@ def start_job!(job) "runAsUser" => username_to_uid(first_docker_executor_hint["user"]), #"runAsGroup" => 134 "fsGroup" => 999, - "supplementalGroups" => [999, 1000, File.exists?("/var/run/docker.sock") ? File.stat("/var/run/docker.sock").gid : 1001] #TODO: fix this hack + "supplementalGroups" => [999, 1000, File.exist?("/var/run/docker.sock") ? File.stat("/var/run/docker.sock").gid : 1001] #TODO: fix this hack }, "containers" => [ { @@ -297,50 +393,56 @@ def start_job!(job) "privileged" => true, #TODO: figure out un-privd case, use kaniko??? #"runAsUser" => 0 "runAsUser" => username_to_uid(first_docker_executor_hint["user"]), - "runAsGroup" => 999 + "runAsGroup" => 999 #T!!!!! #"fsGroup" => 999 }, "name" => clean_name, "image" => run_image, "imagePullPolicy" => "IfNotPresent", "workingDir" => job.parameters[:working_directory] || "/home/app/#{current_app}", #TODO: local executor support - "args" => sleep_cmd_args, + "command" => sleep_cmd_args, "volumeMounts" => [ { - "mountPath" => "/var/run/docker.sock", - "name" => "dood" + "mountPath" => "/certs/client", + "name" => "buildkit-client-certs", + "readOnly" => true }, { - "mountPath" => build_manifest_dir, + "mountPath" => "/home/app/workflows", "name" => "fd-config-volume" }, { "mountPath" => "/home/app/#{current_app}", "name" => "scratch-dir" }, + { + "mountPath" => "/home/app/.config", + "name" => "config-dir" + }, { "mountPath" => "/var/tmp/artifacts", "name" => "build-artifacts" }, + #TODO: configurable secrets/mounts certs/ssh/tmp/etc #{ # "mountPath" => "/home/app/.ssh", # "name" => "ssh-key" #}, ], - "env" => extra_runtime_envs.merge(job.parameters[:environment]).collect { |k,v| {"name" => k, "value" => v } } + "env" => extra_runtime_envs.merge(job.parameters[:environment]).merge({"GIT_CONFIG_GLOBAL" => "/home/app/.config/.gitconfig", "GIT_DISCOVERY_ACROSS_FILESYSTEM" => "true"}).collect { |k,v| {"name" => k, "value" => v } } } ], "volumes" => [ { - "name" => "dood", - "hostPath" => { - "path" => "/var/run/docker.sock" + "name" => "buildkit-client-certs", + "secret" => { + "secretName" => "buildkit-client-certs" } }, { "name" => "fd-config-volume", "configMap" => { - "name" => "fd-#{clean_name}-#{current_revision}" + "name" => "fd-#{clean_name}-#{Digest::SHA2.new(256).hexdigest(job.parameters[:command])}" } }, { @@ -350,6 +452,13 @@ def start_job!(job) "path" => "/var/tmp/polly-safe/git/#{current_app}" } }, + { + "name" => "config-dir", + "emptyDir" => {}, + #"hostPath" => { + # "path" => "/var/tmp/polly-safe/scratch/#{current_app}" + #} + }, { "name" => "scratch-dir", "emptyDir" => {}, @@ -372,6 +481,7 @@ def start_job!(job) ] } + #TODO: document ssh-key bootstrap if executor_hints[:setup_remote_docker] && (ENV["POLLY_SSH_AUTH_SOCK"] || ENV["SSH_AUTH_SOCK"]) container_spec["volumes"] << { "name" => "ssh-auth-sock", @@ -394,7 +504,7 @@ def start_job!(job) "apiVersion" => "v1", "kind" => "ConfigMap", "metadata" => { - "name" => "fd-#{clean_name}-#{current_revision}" + "name" => "fd-#{clean_name}-#{Digest::SHA2.new(256).hexdigest(job.parameters[:command])}" }, "data" => { "run.sh" => job.parameters[:command] @@ -451,7 +561,7 @@ def start_job!(job) polly_waitx = [ "polly", "waitx", - clean_name, + "app=#{clean_name},polly=polly-ci", ] + intend_to_run_cmd @runners << [job.run_name, clean_name, execute_simple(:async, polly_waitx, {})] @@ -726,6 +836,48 @@ def execute_simple(mode, cmd, options) o, e, s = Open3.capture3(*cmd, options) return exit_proc.call(o, e, s, false) + when :async_wait_status + #o, e, s = Open3.capture3(*cmd, options) + #puts options.inspect + $stdout.sync = true + $stderr.sync = true + + i, o, e, s = Open3.popen3(*cmd, options) + + read_io = Proc.new { + chunk = 65432 + begin + stdout = o.read_nonblock(chunk) + $stdout.write(stdout) + rescue IO::EAGAINWaitReadable, Errno::EIO, Errno::EAGAIN, Errno::EINTR => err + #_r, _w, _e = IO.select(process_fds, nil, process_fds, 0.5) + sleep 0.1 + rescue EOFError => err + end + + begin + stderr = e.read_nonblock(chunk) + $stderr.write(stderr) + rescue IO::EAGAINWaitReadable, Errno::EIO, Errno::EAGAIN, Errno::EINTR => err + #_r, _w, _e = IO.select(process_fds, nil, process_fds, 0.5) + sleep 0.1 + rescue EOFError => err + end + } + + while s.alive? + #$stdout.write(".") + + read_io.call + + s.join(0.1) + end + + read_io.call + + return s.value.success? + #return exit_proc.call(o, e, s, false) + when :async stdin, stdout, stderr, wait_thr = Open3.popen3(*cmd, options) return [stdin, stdout, stderr, wait_thr, exit_proc] @@ -922,7 +1074,9 @@ def execute_procfile(working_directory, procfile, obv = ::Polly::Observe.new) $stdout.write($/) end - def polly_pod(label = "name=#{POLLY}-git") + def polly_pod(service = "controller") + label = "name=#{POLLY}-#{service}" + #puts label.inspect @polly_pods ||= {} @polly_pods[label] ||= begin cmd = "kubectl get pods --field-selector=status.phase=Running -l #{label} -o name | cut -d/ -f2" @@ -932,6 +1086,25 @@ def polly_pod(label = "name=#{POLLY}-git") end end + def app_pod + cmd = "kubectl get pods --field-selector=status.phase=Running -l #{polly_labels.flatten.each_slice(2).collect { |x| x.join("=") }.join(",")} -o name | cut -d/ -f2" + a = IO.popen(cmd).read.strip.split("\n")[0] + #wait_child + a + end + + def polly_service(service) + label = "name=#{POLLY}-#{service}" + @polly_services ||= {} + @polly_services[label] ||= begin + cmd = "kubectl get services -l #{label} -o json" + a = IO.popen(cmd) + wait_child + parsed_services = JSON.parse(a.read) + parsed_services["items"][0]["spec"]["clusterIP"] + end + end + def in_polly? current_app == POLLY end diff --git a/lib/polly/generate.rb b/lib/polly/generate.rb index d62c826..95b5de3 100644 --- a/lib/polly/generate.rb +++ b/lib/polly/generate.rb @@ -22,7 +22,7 @@ def all_images @all_images end - def read_circleci_output(ident = nil) + def read_circleci_output(image, ident = nil) jobs_repacked = {} @pl_wk = ident.nil? ? @workflows_by_ident[@workflows_by_ident.keys.first] : @workflows_by_ident[ident] @@ -42,8 +42,10 @@ def read_circleci_output(ident = nil) } ].compact }.merge({ - "docker" => job_spec.parameters[:executor_hints][:docker] + "docker" => [{"image" => image}] }) + #job_spec.parameters[:executor_hints][:docker] + #[{"image"=>workflow_image}], jobs_repacked[job_name].delete("environment") unless jobs_repacked[job_name]["environment"] && !jobs_repacked[job_name]["environment"].empty? jobs_repacked[job_name].delete("working_directory") unless jobs_repacked[job_name]["working_directory"] } @@ -171,13 +173,13 @@ def root end def app - user("app") + user("polly") end def prototype1 @prototype1 = true @bootstrap = image { - stage "bootstrap", "ubuntu:focal-20221130" + stage "bootstrap", "ubuntu:jammy-20221130" root @@ -191,7 +193,7 @@ def prototype1 run %q{test -e /usr/lib/locale/locale-archive || ((locale-gen --purge en_US); (echo -e "LANG=$LANG\nLANGUAGE=$LANGUAGE\n" | tee /etc/default/locale); (locale-gen $LANGUAGE); (dpkg-reconfigure locales))} - apt %w{curl mysql-client-8.0 mysql-server-core-8.0 ruby2* libruby2* ruby-bundler rubygems-integration rake git build-essential default-libmysqlclient-dev} + apt %w{curl mysql-client-8.0 mysql-server-core-8.0 ruby3* libruby3* ruby-bundler rubygems-integration rake git build-essential default-libmysqlclient-dev} run %q{useradd --uid 1000 --home-dir /home/app --create-home --shell /bin/bash app} command("WORKDIR") { "/home/app" @@ -206,6 +208,7 @@ def prototype1 run %q{bundle config set --local jobs 4} run %q{bundle config set --local retry 3} #TODO: figure out conventional bundling strategy + #TODO: figure out .gem strategy prototype #run %q{bundle config set --local deploment true} #run %q{bundle config set --local without development} command("COPY") { @@ -309,6 +312,10 @@ def job(*args) @this_plan.add_circleci_job(*args) end + def container_image(filename) + emit(File.read(filename)) + end + def plan @workflows_by_ident ||= {} @@ -331,6 +338,14 @@ def test(plan) @shell_commands << ["polly", "test", "--ident", plan.ident] end + def build(container_image) + @shell_commands << ["polly", "build"] + end + + def deploy(instance) + @shell_commands << ["polly", "deploy"] + end + def read_shell_commands @shell_commands.collect! { |shell_cmd_array| shell_cmd_array.join(" ") diff --git a/lib/polly/plan.rb b/lib/polly/plan.rb index 5900c04..d6b78a4 100644 --- a/lib/polly/plan.rb +++ b/lib/polly/plan.rb @@ -206,28 +206,34 @@ def load_circleci(raw_yaml = File.read(DEFAULT_CIRCLECI_CONFIG_YML_PATH)) raise "empty config" if raw_yaml.nil? || raw_yaml.empty? yaml_template_rendered = raw_yaml.gsub("$CIRCLE_SHA1", @revision) - circle_yaml = YAML.load(yaml_template_rendered) + circle_yaml = YAML.load(yaml_template_rendered, :aliases => true) return unless circle_yaml && circle_yaml["workflows"] && circle_yaml["jobs"] add_job_to_stack = lambda { |job_run_name| + circleci_like_parameters = circle_yaml["jobs"][job_run_name] image = nil - if exe_found = circleci_like_parameters["executor"] - exe_name = exe_found["name"] - if exe_name - image = circle_yaml["executors"][exe_name]["docker"] - else - image = circle_yaml["executors"][exe_found]["docker"] - end - else - image = circleci_like_parameters["docker"] - end + + #if exe_found = circleci_like_parameters["executor"] + #raise exe_found.inspect + # exe_name = exe_found["name"] + # if exe_name + # image = circle_yaml["executors"][exe_name]["docker"] + # else + # image = circle_yaml["executors"][exe_found]["docker"] + # end + #else + #image = circleci_like_parameters["docker"] + #end + + #raise image.inspect #TODO: Regen module #puts "add_circleci_job(#{job_run_name.inspect}, #{image.inspect}, #{circleci_like_parameters["steps"].inspect}, #{circleci_like_parameters["environment"].inspect}, #{circleci_like_parameters["working_directory"].inspect}" - add_circleci_job(job_run_name, image, circleci_like_parameters["steps"], circleci_like_parameters["environment"], circleci_like_parameters["working_directory"]) + ##### !!!! + add_circleci_job(job_run_name, circleci_like_parameters["steps"], circleci_like_parameters["environment"], circleci_like_parameters["working_directory"], circleci_like_parameters["docker"]) } circle_yaml["workflows"].each do |workflow_key, workflow| @@ -254,12 +260,15 @@ def load_circleci(raw_yaml = File.read(DEFAULT_CIRCLECI_CONFIG_YML_PATH)) end end - def add_circleci_job(job_run_name, docker_params, steps, job_env, working_directory) #, pre_calc_dep = nil) + def add_circleci_job(job_run_name, steps, job_env, working_directory, image = nil) executor_hints = { - :docker => docker_params #TODO: || "polly:latest" } - #steps = circleci_like_parameters["steps"] + if image + executor_hints.merge!({ + :docker => image + }) + end pro_fd = StringIO.new @@ -324,6 +333,8 @@ def add_circleci_job(job_run_name, docker_params, steps, job_env, working_direct :command => pro_fd.read, :working_directory => working_directory, :executor_hints => executor_hints + #.merge({ + # :docker => docker_params #TODO: || "polly:latest" } if count_of_steps > 0 diff --git a/polly.gemspec b/polly.gemspec index 286583f..3b05774 100644 --- a/polly.gemspec +++ b/polly.gemspec @@ -18,8 +18,9 @@ Gem::Specification.new do |spec| spec.executables = ["polly"] spec.require_paths = ["lib"] - spec.add_dependency "thor", "= 0.20.3" - spec.add_dependency "net-ssh", "~> 6.0" - spec.add_dependency "yajl-ruby", "= 1.4.1" - spec.add_dependency "guard", "~> 2.18" + spec.add_dependency "thor", "~> 1.3" + spec.add_dependency "net-ssh", "~> 7.2" + spec.add_dependency "yajl-ruby", "~> 1.4.3" + spec.add_dependency "rack", "~> 3.1" + spec.add_dependency "rackup", "~> 2.1" end diff --git a/spec/lib/polly/execute_spec.rb b/spec/lib/polly/execute_spec.rb index 44803dd..46adb94 100644 --- a/spec/lib/polly/execute_spec.rb +++ b/spec/lib/polly/execute_spec.rb @@ -8,10 +8,10 @@ context "performing locally" do it "has a simple method that exits on failure" do - expect(Kernel).to receive(:system).with('true').and_return(true) + expect(Kernel).to receive(:system).with('true', {:err => $stderr}).and_return(true) exe.systemx('true') - expect(Kernel).to receive(:system).with('false').and_return(false) + expect(Kernel).to receive(:system).with('false', {:err => $stderr}).and_return(false) expect(Kernel).to receive(:exit).with(1).and_return(true) exe.systemx('false') end diff --git a/spec/lib/polly/plan_spec.rb b/spec/lib/polly/plan_spec.rb index fe8718d..7c207ea 100644 --- a/spec/lib/polly/plan_spec.rb +++ b/spec/lib/polly/plan_spec.rb @@ -281,8 +281,8 @@ def plain_workflow(plan) #TODO: more testing, wtf was this for ???? puts "debug stuff here 3" - plan.add_circleci_job("bootstrap", "ubuntu:latest", [{"run"=>{"name"=>"bootstrap", "command"=>"true\n"}}], {}, nil) - plan.add_circleci_job("primary", "polly:latest", [{"run"=>{"name"=>"rspec", "command"=>"bundle exec rspec\n"}}], {}, nil) + plan.add_circleci_job("bootstrap", [{"run"=>{"name"=>"bootstrap", "command"=>"true\n"}}], {}, nil, "ubuntu:latest") + plan.add_circleci_job("primary", [{"run"=>{"name"=>"rspec", "command"=>"bundle exec rspec\n"}}], {}, nil, "polly:latest") plan.depends("primary", "bootstrap") plain_workflow(plan)