group :test, :diagnostics, :performance do
gem 'minitest', '~> 5.10.3'
- # Selenium-webdriver 3.x is producing problems like the one described here:
- # https://stackoverflow.com/questions/41310586/ruby-selenium-webdriver-unable-to-find-mozilla-geckodriver
- gem 'selenium-webdriver', '~> 2.53.1'
+ gem 'selenium-webdriver', '~> 3'
gem 'capybara', '~> 2.5.0'
gem 'poltergeist', '~> 1.5.1'
gem 'headless', '~> 1.0.2'
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- childprocess (0.8.0)
+ childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
cliver (0.3.2)
coffee-rails (4.2.2)
extlib (0.9.16)
faraday (0.14.0)
multipart-post (>= 1.2, < 3)
- ffi (1.9.23)
+ ffi (1.9.25)
flamegraph (0.9.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
retriable (1.4.1)
ruby-debug-passenger (0.2.0)
ruby-prof (0.17.0)
- rubyzip (1.2.1)
+ rubyzip (1.2.2)
rvm-capistrano (1.5.6)
capistrano (~> 2.15.4)
safe_yaml (1.0.4)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
- selenium-webdriver (2.53.4)
+ selenium-webdriver (3.14.1)
childprocess (~> 0.5)
- rubyzip (~> 1.0)
- websocket (~> 1.0)
+ rubyzip (~> 1.2, >= 1.2.2)
signet (0.8.1)
addressable (~> 2.3)
faraday (~> 0.9)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
- websocket (1.2.5)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
safe_yaml
sass
sass-rails
- selenium-webdriver (~> 2.53.1)
+ selenium-webdriver (~> 3)
simplecov (~> 0.7)
simplecov-rcov
sshkey
wiselinks
BUNDLED WITH
- 1.16.2
+ 1.16.3
items = []
container_uuid = if @proxied.is_a?(Container) then uuid else get(:container_uuid) end
if container_uuid
- cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts)
+ cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts runtime_token)
my_children = @child_proxies || ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results if !my_children
my_child_containers = my_children.map(&:container_uuid).compact.uniq
grandchildren = {}
<i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
<% end %>
</li>
+ <% if Rails.configuration.repositories %>
<li role="menuitem"><a href="/repositories" role="menuitem"><i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories </a></li>
+ <% end -%>
<li role="menuitem"><a href="/current_token" role="menuitem"><i class="fa fa-lg fa-ticket fa-fw"></i> Current token</a></li>
<li role="menuitem">
<%= link_to ssh_keys_user_path(current_user), role: 'menu-item' do %>
<li role="presentation" class="dropdown-header">
Admin Settings
</li>
+ <% if Rails.configuration.repositories %>
<li role="menuitem"><a href="/repositories">
<i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
</a></li>
+ <% end -%>
<li role="menuitem"><a href="/virtual_machines">
<i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
</a></li>
<%
container_uuid = if @object.is_a?(Container) then @object.uuid elsif @object.is_a?(ContainerRequest) then @object.container_uuid end
if container_uuid
- cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts)
+ cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts runtime_token)
reqs = ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results
load_preloaded_objects(reqs)
# Link to use for Arvados Workflow Composer app, or false if not available.
#
composer_url: false
+
+ #
+ # Should workbench allow management of local git repositories? Set to false if
+ # the jobs api is disabled and there are no local git repositories.
+ #
+ repositories: true
def selenium_opts
{
port: available_port('selenium'),
+ desired_capabilities: Selenium::WebDriver::Remote::Capabilities.firefox(
+ acceptInsecureCerts: true,
+ ),
}
end
# DOM element.
def assert_triggers_dom_event events, target='body'
magic = 'received-dom-event-' + rand(2**30).to_s(36)
- page.evaluate_script <<eos
+ page.execute_script <<eos
$('#{target}').one('#{events}', function() {
$('body').addClass('#{magic}');
});
eos
yield
assert_selector "body.#{magic}"
- page.evaluate_script "$('body').removeClass('#{magic}');";
+ page.execute_script "$('body').removeClass('#{magic}');";
end
end
screenshot
end
if Capybara.current_driver == :selenium
+ # Clearing localStorage crashes on a page where JS isn't
+ # executed. We also need to make sure we're clearing
+ # localStorage for the test server's origin, even if we finished
+ # the test on a different origin.
+ host = Capybara.current_session.server.host
+ port = Capybara.current_session.server.port
+ base = "http://#{host}:#{port}"
+ if page.evaluate_script("window.document.contentType") != "text/html" ||
+ !page.evaluate_script("window.location.toString()").start_with?(base)
+ visit "#{base}/404"
+ end
page.execute_script("window.localStorage.clear()")
else
page.driver.restart if defined?(page.driver.restart)
set -e
-if [ "%{name}" != "%\{name\}" ]; then
+# Detect rpm-based systems: the exit code of the following command is zero
+# on rpm-based systems
+if /usr/bin/rpm -q -f /usr/bin/rpm >/dev/null 2>&1; then
# Red Hat ("%{...}" is interpolated at package build time)
pkg="%{name}"
pkgtype=rpm
set -e
-if [ "%{name}" != "%\{name\}" ]; then
+# Detect rpm-based systems: the exit code of the following command is zero
+# on rpm-based systems
+if /usr/bin/rpm -q -f /usr/bin/rpm >/dev/null 2>&1; then
# Red Hat ("%{...}" is interpolated at package build time)
pkg="%{name}"
pkgtype=rpm
--- /dev/null
+*/generated
+common-generated/
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+all: centos7/generated debian8/generated debian9/generated ubuntu1404/generated ubuntu1604/generated ubuntu1804/generated
+
+centos7/generated: common-generated-all
+ test -d centos7/generated || mkdir centos7/generated
+ cp -rlt centos7/generated common-generated/*
+
+debian8/generated: common-generated-all
+ test -d debian8/generated || mkdir debian8/generated
+ cp -rlt debian8/generated common-generated/*
+
+debian9/generated: common-generated-all
+ test -d debian9/generated || mkdir debian9/generated
+ cp -rlt debian9/generated common-generated/*
+
+ubuntu1404/generated: common-generated-all
+ test -d ubuntu1404/generated || mkdir ubuntu1404/generated
+ cp -rlt ubuntu1404/generated common-generated/*
+
+ubuntu1604/generated: common-generated-all
+ test -d ubuntu1604/generated || mkdir ubuntu1604/generated
+ cp -rlt ubuntu1604/generated common-generated/*
+
+ubuntu1804/generated: common-generated-all
+ test -d ubuntu1804/generated || mkdir ubuntu1804/generated
+ cp -rlt ubuntu1804/generated common-generated/*
+
+RVMKEY=rvm.asc
+
+common-generated-all: common-generated/$(RVMKEY)
+
+common-generated/$(RVMKEY): common-generated
+ wget -cqO common-generated/$(RVMKEY) https://rvm.io/mpapis.asc
+
+common-generated:
+ mkdir common-generated
--- /dev/null
+==================
+DOCKER IMAGE BUILD
+==================
+
+1. `make`
+2. `cd DISTRO`
+3. `docker build -t arvados/build:DISTRO .`
# SPDX-License-Identifier: AGPL-3.0
FROM centos:7
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
+# Install dependencies.
RUN yum -q -y install scl-utils centos-release-scl which tar
# Install RVM
+ADD generated/rvm.asc /tmp/
RUN touch /var/lib/rpm/* && \
- gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+ gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3 && \
# SPDX-License-Identifier: AGPL-3.0
FROM debian:8
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
ENV DEBIAN_FRONTEND noninteractive
-# Install RVM
+# Install dependencies
RUN apt-get update && \
- apt-get -y install --no-install-recommends curl ca-certificates && \
- gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+ apt-get -y install --no-install-recommends curl ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3
+++ /dev/null
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: GnuPG v1
-
-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
-/4cVEpiwwKMkxIWrseXTtsKJ8+hWhElzLL8cNTFCz+4ax7PKE8eCGTUXcyMyzAe0
-SysHI2bCWFfQe8RGEy+1cnLH5+qO2MwLDKKN4+at2ADZFrYn4FEZ8wvlb9/is1iN
-7h7+s2GDPpvEJrrnpeHeNTX1BEeykvbjW+Bsb89g6kJfks7PvhD+BQwVMaNrmjR8
-KYi+GaopHdQ30yLWD4ck97B+UYZnqSKk7E8NOpwU22b31VlZgI/JXWeXgQ5cY58J
-0ShGmbfypHXerthzlm6UHzjg6g+yujqDrxaOPTdfxYAzJmX5ztk3D9xMEhCAD8yp
-efTdMGQb0B2q5Yjsl3sHqNNqxTeg1iorUNz6UtEhX3RmGrTDdY0rgn0prjgiMTEM
-WWxAmgeYw/gOkpWfEZNrX64La65OkhTg+aBZEe6broHrc1x6dXpoRkxGJrlRfcsx
-Ocgy2M0/C0gk1L1yYWXRL4yENkQ/hNuPZmdDtrvWxcSTnMPfclwq6kklflC099uj
-CZf5NuOG80mZi03917LQBtgWp4INmG+B6aLY52KuINmWXm25wYPY5MfEbyO2Kzng
-JSpxdR90GbP4F5lri2Ho9eSByisb0dLq0ugBEAABAQAAAAAAAAAAAAAAAP/Y/+AA
-EEpGSUYAAQEBAGAAYAAA/+EAQEV4aWYAAElJKgAIAAAAAQBphwQAAQAAABoAAAAA
-AAAAAgACoAkAAQAAAJ8AAAADoAkAAQAAAJ8AAAAAAAAA/9sAQwAIBgYHBgUIBwcH
-CQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04
-MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
-MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAnwCfAwEiAAIRAQMRAf/E
-AB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE
-AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBka
-JSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SF
-hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY
-2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgME
-BQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKB
-CBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNU
-VVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip
-qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/a
-AAwDAQACEQMRAD8A9YFKTzRikxikSLTutMpw6UAKBRxQc9qOaYCe1I2BznisHX/F
-dnoSFW/fXH/PNT0+p7V5J4k8calrExRZXgg/55xEgd+vrWsKTlqcdXGQjLljqz2T
-UvEekaTj7ZfRIx/hzk/kKoQ+PvDMzhBqiBiM/MrAfnjFfP085kIBYsR+NN+6gYnB
-7YrT2MTH63U3PoR/HXhqPJ/tSM4/uqx/kKt6d4p0TU5PLtNSheT+6x2n8jivnaJg
-Y8k80hlYEccU/YR7krGVL7H1CCGGVII9jTq+crDxXqWkSIba8lXB4Utkfka9C8O/
-Fe3vJEt9Xg8h+nnpypPuO1ZyotbanRTxal8SseldTQRmo4Z47iJZInV42GQynINS
-8isTrTuJ04oNGaSgYn1opN3PSlwKAHGlptLSAMYpRSHmkBzQA+qerX8emafLdSZw
-o4x69qtj61w/xGvzHawWcbcsDI/8h/WtqMOeaRy4ys6VFyW55V4nvp7jUZHMpO87
-ic9c1iIk0gOwE+tdr4Z8HXPia482bKWqHBf19q9V0/wHo9lEgFsrMv8AEwrerVjB
-2OHC4WpOmrfefP8AHpV4SCsTE4yRtNWf7DvPL3NA+BzX0eugWIx+4TI74qZ9EtGT
-aYUwO2Kw+sdkdf1CT3kfNqac8Iw8Lg9uKryQYypVlr6Kn8L2MnWFfxFYOqeB7O4B
-8uMBs5qvrK6oyeW1L3TPBpok5yeRUEbmE9Rkd69UvvhqzZMbY9hXI6v4Lv8AT4jI
-IyyDrgU41oN6Ml4erFe8tDX+H/jT+xrs2d27NaTsOevln1r28HIyDwa+UlLRTLkY
-IPIr6D8AazJq/htDK+6aBvLY47dv0pVVdcyNcNPll7N9djqqPwopCawO8UYopM4p
-RQIWiikpDFopKUelAB0ryzxFu1vxXNDGSwVxGMdgOD+ua9L1C7Fnp9xcn/llGWH1
-xxXE+DLLdI91McyO3X1rswz5IyqM8jMr1alOguup3Oj2EWnadFbxIFCqK0Vf3qGP
-g1IUJrz27u579OKhFRRMHWnF6r7HHSj5h1oTG0Pds1A4ycU4saruzFj1pNlJCPtH
-XFUrm2hnQo6Ag9sVM6uxpqoQcv0HSsy2lY8Y+IHhRdLuRe2qfuZDyAOhrY+EN7i5
-v7Jj96MSAfQ4/rXceJLOPUNJnikGRtJFeYeCQ+keO7dTxHKWj9sEcfrivRo3nSZ4
-OJ5aWJX3ntxNBGaM8ZNJmsDvCnA4ppOBSr05oAdTSCehp3egdaQwxx0oXmlPNC0w
-MLxZNjQZEx991UjPXnP9Ki0O2ECxLtClVHA6CtTV7VJbINMuUVw2fTFRWbots04O
-VY8EelaSqpUeVHFDDOWN9pLayNiLnGasBgvU15hqHj+5hvJYoIY/KQkBt3LYrNl+
-J8qEHy2Udw1cXOj2eRs9kDrTXkQ8V5Rb/E5JpY02qV43EHmuj03xTFqJAjbLdcU/
-aIapNnYYQ0AR461zF/rn2Ncvwvqax5/H1raMqyc5HJz0o50ynSaV2d40aYzVW4jG
-w4rjofHkFzIRGvy4zuLDApZPH1hFtWSVMdCcjj8qTkibNGnqEhWF0znjpXmN0wtd
-bimG4NFKGBHsc1341O21NfMgkDoRwVrz3WAU1p1zzu/Wu/AT1cTw88pe7Gouh7Wr
-CRFdfusAacR6CobLIsLcHIPlr/Kpj3rJ7nbB3imwwB9aKO2KXoKQx1Hc0gIYceuK
-WkMOcUo6UtJmmBkTXEpa8t5OYyxMftwOK4qHWdQ/tk+Gm067gtS7bbwblV1CltoO
-Opxjg5xn611mr3xi1G3tkUEyuAfp3P5Vb+yC4lt5N20wymQDHXKMuP8Ax7P4Vzc2
-8TtlT1U32PLdU2WDymS0Z33HYqpkmuZvdbv4wG/sdVB7OCT+QHvXvN5o0Nyd20Bx
-0OKxbvw/MxwGVvrGP51Cunqa2jJaOx4zBdedKgn06FHk5Uxg5/nXZ+GUe01GHbbz
-ziVWIiUqGGMc/MQMc+ueldInhFFkEsuxSDkAKM1P4Q0Ce38VX15PK8sUOUi3LgLu
-wcD6KF5/2qe7B+6tDO8ZTzHSXdtMvoQAMtI0RA+u1yf0ry65mtkYLKkszdBtcpn+
-dfQ/jSx+3+HLy1jQGWSFlQ4744rynTvC8l7plvcNHHM7rnDDBA7c+uOvvVNKLJi3
-NJHBrqOnLN5UunzhunMpbH4VejXTr1SYUYEdU3kGu0HhJGfJ0rcwPJ4B/OpY/BCm
-UObVYMHs2c1LmuhSptblLQZzpukvcxu6wQHdLHJhsL/EQQBjA579KNOvNN1rVxfK
-fPV5QsaEFAAoyzH17cV1FxoUdvpUttGgYTLtdW6FSQD+ma5u/sbKzuo47OOK23KV
-CxqFGMjPArSnV5Nepz1sOqrUbXVz0fS/EEWqapLY28X7qCIM0o6E5HArbzXIeCrQ
-W816/UuEwfb/AD/Kuv8AerpNtXZGJjGNS0ULj1opKU1ocw6koIyKO1IoXPpTQRmj
-gUnfNAjPvLZZLn7UOHhUqQR1zwCP1FS2uMBiakupl+zSRZAbg47nmsee++zoornl
-ZM9GF5JXN8yM3AbH4Zqvc3HlIWecAe6isT+1gI8lv1rm9T1aXULlbSBiWY4PPQVL
-qHRGjrrsdNb6gdVuzBby5C/eYDFdLZbEUAMCe5FcLL5uhacJLBVabbhgf4q5608Y
-ana+ZLcxbWJ6K3H60ou24VIKXw6Hq+pS5Bwa4hL6bTdTMFzs8t+YziuW1Xx7cy24
-SMNv9FNOsdRfV4A1zuAjGFz2z1olK+qHTgo6M9LimV0DbF6fwmkYxsT8pP1ri9K1
-p7aT7PKxO3hTnqK15dXXH3qXPcbpW2J9VlKwMEAAx0FcZf2TXV7ayIf3pymPbI5/
-nWzNqRugyL0xirVlYPd2jBGMZJwXA5x6Z7UlqZr3JHSeH4Fg01SCDuOM+oHH+Nap
-6VBawJa2sVug+WNQoqftXalZWPKqS5pNhmgmkx6UgAzTIJc+tHem5BpQBSGBBzxQ
-aUUNTEUry3ilQyMgLoCVboRXNX6GTdt7jNdc3PBHFcs58u4eGTqrFT/Q1z1kduFl
-0MDUDLHZkR5J6VDo1stvNvndPNbkjPSt94IyxRhlT61gatocl2ri1uGtps/LItcy
-0Z36yVjqmCSxBcjkd/WuU1rRDJGTCAWHUKetYFnpWsRytb6pfTBlYYmDfKQT+nFa
-z6C6Ws8qa0A8TEAhxjoCM+nWtHOzsxKCW7MgeHnRi0oXJPc1q2tvFZQshdeTyB6V
-WvdDEflmbWkZmVmJMg5x6Vyk1hqt5dJDZTSRx/xSliM0lLmdinFdDpLgGWTdG4BU
-8VYSSeS0VjkE0zR/D+x0SadpnH3nY9a3rm3iR44kXEaDpUS3Hdoz7SNojljyeTXY
-aJpeokQyyzQrZkB1Vcl2zzg8YH61yrZmu44IgN0jBF+pOBXp8aLFEka/dQBR9BW9
-GF9WcGIquLshcYNO7Yph60ZJrqOAdnPamUo601zhc5xQMzBr1uf4Jf8Av2aUa/bZ
-+5N/3wa1/sdt6/8AjtIbK37MP++anUNDK/4SK1z9yb/vg0f8JFbY+5L/AN8GtQ2l
-t/eH/fNH2W19R/3zR7waGWdetM5xJ/3waw9avIZblLmDcMjEgKkfQ1132W1J+8P+
-+ajubC0ntpYm2kMpHSpkm0aUp8srnGi7VwOOfWpoGLHGec1iBzDcPbt1UkCtiwAe
-ZW3Ae2a5Op6qlbULqSNP9am0f3sZx9axbiXSjIPPt9Pdh/G2M/ka66501LpfmyAa
-y5PBtnMN0hbJ9MCrXMaxqxSOamm0wcW8dlFzyY1GT+VPtdkjZjTIHG4962JPClpa
-NlGY5PGcVcisIbVBjkHnPtUyuVKomjOgH2bLSdevFZ9xeDc0h4z0qa/u1ZiiYxzX
-O311j5FOT0qLGLlc6HwxPbf219ru5VSOAEru7uen5DP6V3X/AAkOl97yL86860W/
-utNgjEOlJqKPukmTftlAGOUzwT7e1auo+KNAis7W/wDskkljckqk0aA7HHVHXghv
-5jmuum9LI83ERfNdnZf2/pZHF5Efxo/tzTyeLhPzrmtP1XwjehjHqlmhHJE2Yv8A
-0IDP4Vv2lhpF/Hvs5rO4QdWikVgPyrT3jn0JxrNiy5NzGPbdzVS91m1KMqXMYAwS
-xNW20Cz7RQ/pXP6vokC2o2qil5OCCPQ/4VMnKxUUrncKOelLt4zg4p5ULGXLAAcm
-mWWoQ3UX7p1kGSMocim5paFxpSaG7Qe1IAOeBx19q8+1P4iz6H41udLvIEnsEx80
-I+dBgc8nB69OK868UeJv7X8R3M9rNcixmA8uOU4KnA3AgEjrmqV3sQ0k3c9e1nx5
-oWjlkNz9pmGcpb4bH1boKQeINSlhtoRbRQXlwqTSggt9ljb7qnP3pGAJ7BQDxxz4
-lpaR3Otabay48ua7iR8/3S4B/SvV9O1Rr/xNqCSqVddQus577QI1/JVrOu+SJvg6
-Ptajvsk39xW1m1InNxGPmzzVaC+dIhIjZI6jFdJdQhlYYzmuU1K0ls5TNBnbnkdj
-XIjs3RtWvi2AAo/GOoParra5HIg2uB3zmuDmFrdncy7JPbiq7LcRf6ufIHTNXzMu
-MTurjXYoSWkmDccVzt/4qeZWSMfMRgAdhXPS+dJ/rZSaZujg6fM3rSuNouTXRVOS
-d3qaqW8bTzbjzzzSQwS3cnfHf2roLKyWJRxj8Km9hWHed/ZiWMynDLcA4/2cc/pX
-OpcJqGkeOIo8G1jmS8g9FbzduR9VYitDxBcqk6gt8sEZZh6f5xXOaIxtvh94iumG
-Ptlxb2iH1IJkb9APzrTDNuUjTMqSp4ei+ru/l0MMTFcZ65qzpN9e6XqY1CzupYZw
-do2twR6Edx7GswOS3TpVpD0Ga7zxD0bSvidqtoxGoJHeRMDycIwOOMEDBH4VatPH
-EmsXCW17bwRIWLI6ZGMBuDk+9eY7y0p54UfmasRzHJANRNFQ0Z7hZfEfTF0aO8up
-M3EgwIFOSG7gj/PUV5/a+NbnSTqsWlBYILucyIp5MQPZfTtXFJIUj445JpQ/7z68
-1MaKW5rLEN7aE97eyzXstzM7SySYLs3U1UuPnlQLx8vFK53TAdiMUw5Gz/ZO2trW
-MG23dkkNzLDLHKh2zRsHQ+jA5Feoalr2n6bqlp4hiObK+ZbiTaPuGQEP+T7vyryq
-YEHIPvXQeHbGLxHZPoctw0TyygW7kEhXYEqD7Ejn65rnxEOZL1O7L6nJKfnFnssd
-zDdwpNbyrJFINyuhyCKr3NsJoypGa8P0fxPq/hK8ltAweKNystvIcruBwcEdD9P1
-r07QviFo+sFIJPNtrlv+WboWBPsR/XFc86UkOnWi9yvqGj/vGZFx9DWHPbXUJIJY
-fUV6RcQJIm7gg9DWFd2KnPzY/Csrs6YtHEMs7NyGwfbFWrbT3lYFhWldNY2AMl1N
-sX12k/yFYt3470mzBSzhluHHfGxfzPP6U0pS2QpVIx3Z1FrZrEg6ADtWN4g8X2ek
-K0Fsy3F50CKeFPuf6VxGqeMtV1MNGsgtoTxsi4J+p61labD5t/EX5VTvOe+Oa1jR
-sryMfbe0moQ6nRaxdztZJC7mS6uCA+Op9f14q74q2aNpel+GEJ8yyQ3N7/18SAEr
-/wABXaPzqbwv5MU974tv4/NtNJ2+RD/z0nY/uwfQA5Yn2FcleTz311JcXEheeeQy
-SuerMTkmtqMOWIsyxKr1vd+GOi9EJGmRn1qyPlGT2qNBgClkYCPHrXQecKudnPU8
-0/fsHqe9IKhmbFTLYqL1P//ZiQIiBBIBCgAMBQJZQK6VBYMHhh+AAAoJEDbDVqWm
-6mZ2sKkP/iFl4nK/VGMkw9vnCiwJSx4ooQRv+aG1w70R41tfbsSijRQuC497au0I
-ihdxKRv8dzAUHyOp4ahmPee20kturZEz8p4zI61c707WoFfOypM/cm2yV0eyEr0/
-sHaPDkrgtaGYO32pKXCXg6coXUgQz7lPhpU8FABmWIETopBdv4Waf/5yyXblkqgd
-730g1NsVvdFN1+OrYQ/8UUFnf3iv3gE36HkrM7UWSXJL/yYmvF2MFqRQliFGT1O9
-ZPwyc/SOgZBVnBIH9SMKE7ty31y4a+M869opf8Xr192jK6RpbFkvV6mVCiU6te4p
-7bjXnYb3Z34WJNkppSdrH2oiw8W2dypRKvu4X6D6pRhBtG5FQKSCRn436PcY6Qz5
-2e3LFq2R62IrB92fHidZbeQKX0TFRtqyOG/wl4C7CEdJ31wwoS9Jj2kH66g+e4FI
-vE5LJM+Y+imyFerGfh3FIJ0nJYVf9NOtEl2uy5ZoTa3bVKJG4LU/qOLFh1oA7YN0
-zwvaS83WlDKv8jyGBXVQdgr6dhlYCINr8ZimGNbGbkOSSsMYxdg3Hl1dl5+Ok8ih
-O0/F7EbCNX8XSEHIQ+Ie/dees+KajHioghskAFHr5YE/S24YMfAkQtHhZWxPNr0q
-gql0OtaI2e1GG5FMqssWpBksCZykkl6eprfC0uWOymz0t3z400O8iQI3BBMBCAAh
-BQJYseahAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEDgEu4LTncDjYawP
-/0CafFAJq4OAlFl1bhb4BTiw3eAbRnifGpBUldTpxxp65UJyXtqA3wX3ggbhXMWn
-b5zMOh06HzVUFJ7Snse3cxAHgi9+qqueBI53Zx1btPlJNX/AIv6TlESRMZJXE3y2
-pSBdmwW/tiXr5fC5RJqpgUevKfq3eW33aUtybE48FqmLqDJjyAvAbF/jKHY0Zahz
-7iCVkWJwufoGxkcIMMLYh1R633DT2yDSti4xS8xKUYJsiiw2B2t//Ux6zoNh3gzB
-EeMoRDRFPuC3ibfdkwC9zMkbDMNSMSlSB8F0sn52k93uQcnR1jWoWc5nSz9x5m1I
-3xo0Zl0I5YNIkDINz0SL426fN9zBSPHPnXsoOXOnDSF5xGt1Wzg3Do/kErFzlKPE
-xqo5j+W5RbRznuZR+uQw0nvCNGS3kfASbAaXR5lkb0VJiIliLM5HzXKi6W662y/X
-X5iQDkWx7UjjmTx5x7r12RYA6mwLJWlpNQoK4kWDlllUn1QpC+wqjTKePPMM/1vy
-qlh14tRVurxWTZPAh6/2kzrb0a0noq4YBeCNjOU2Ri+1RXZJkthjn2eNsXEvLCp1
-04H7W0uHwDAofiOkX99T823H6dIUEllIyiX5DRyRimZYqC6ux5Qq8AKVJmAKY3+z
-61W+psmFqVXckSMu8fd03DXvVgx60WWgGkGj8hEcoBe3uQINBFRQBC8BEADRTWbS
-pwvuyjGkPQxCsWs2df7qxEbj+NvzETDY7yeyWN49zll0zz1pCYZ11BtzCSiZWTLe
-9Ngk+PJsP4t87gZMwhNgG4YvIEJ9BIcpkPDCMvMyMW6Y8J0rPPjGrscLAPnIQg8V
-TZmsTGeEBUXsQNHFigeBH/OL0Rwf4ydhUt4SfgldrcBCfS2EkJH4ULhUw5enhkfp
-5tt3k5l0C9Wa6+qWYMEdhnN6rBjXGfwksgZAh2VfW7BNbidtuaCn8ZqrSf4p0kC3
-YdClW0wcDSGfT+PpOE04waycxUMB7gH8wvI8phcMSHj7rRJPui6rXytS14AI0BKi
-nfXh0Tw9QwU6V8CxWwq0/SFwcD10Q8Y7gDLfevzptKISpN05ACNSqCf1WmBZn63F
-Wxk68r3JYrxFEYQvVWHMAOQB1doODgrv+84xTxhcn781SAxA/F1BOtMgf/wgiUXU
-Clxn7k4Cf27kLiZb471SM8GTjoi5IVg7WyvN3p9wI6KWy7IrrvuwZKxXvr65Q3iD
-liVP1j0236PaBGJtgznNhaHPyWkaDwBZW9MXQlejc+ixaeTceE9BqKvn+0Z1Dhx7
-WCzfBPgDkqSVLQP5O9kg9qjxkN2TkuQlWVVNJL+0reeQm7CSEyA5SjtZjOmnC9bc
-HDsnLlzPw8Qt9g9OIh8XUhr6inF3k6UrB55nqQARAQABiQREBBgBAgAPBQJUUAQv
-AhsCBQkCPCsAAikJEDgEu4LTncDjwV0gBBkBAgAGBQJUUAQvAAoJEOIGwp+/BP8X
-No8QALBbvo3Dv6Sr8osRYpaGz87Yn7Z5OTUNtO3lQOa1eq/1Fdp4AVJ9+WBqaLdc
-5bXr1xrOoaUu457zrUYB2Bo18VRHRv6hW6qhzDoY2zbUGCyQbrD2SPi94SJogwro
-qXcundbjxrl24mfowskY9RbC2wOx0RhxxapB+mMe2DNxSVeFSszsO6QayzOvXxrt
-FlhVqgn+9BK63mbnbBdRDo46clADCTt5LSl1CETzR0oswI4MQxVtoZJGyC3gVG6u
-kMuUJLfivbS6y9PDJaF0mIkZwf2iKgxfpinNNdvdipJlEstgBV98XK2Q3cD2Qpp/
-btrG0PssXpNuXKm8htKgPYoY64f49VSCzbPJF1IJSOqmo+NGlngZVPpAo3nSCNkD
-Y1osnvtKW7a5uddlVFGhpusWR8hP+YsvV8qIIuC+69cT8RBv59nSECVM5E6bst2Q
-2aLf3l2HOqzIQxq1lwZN1cuI/33mKDIWlms2GX/YzlOsAh6FBzPC4cBNq4BJOuX9
-NciBqDG2vHt+9jf95TypfC1KGCd+pPexy7WqUnsDynu/d3uo7Dh90hhlSUUCdwYS
-n8aOtMTU4t9WkM9JnV+I2g4hkElwCsH4zvJBGxRLpyNOk3FwmwQ/zTM+jJ2mwugm
-wru+rxdryBY1wJ4e7JxZpiS/f9BSj5xwJ9TlfkVT40CcaL7Ye30QAJs8s3ga3axf
-kM+nbUt8TureSPIOF39j6wB9zIq961qpfiKaYoOyy3zX1I/A2SSCJzzxjSwaZooI
-svvu2RiFZUC/1y9qKPpJ6GxKLVZ4H8xvXMGRkNSfRqtIigUkKOF5hmjwfx4jMfyH
-encW4lyrUUEfPymGF+meya5Qvm847HoVU3O24jGGHMZJpt55n30RyodyIl2xrG5h
-A/82Hhsi3i+/mQyVSesQa0GKOqAUp2CHJDgo437LXXuBQFrykFhrSMHqa6sM/YaH
-pMWtIaqLPuEmAmcPf2FpmY2cYIldlo8ImiKBF+yRch2d7dj4A/p5wBvRq09NYlCV
-m0RrmPNtfz0j8YCGx0dEPAmvvWw3P+H/cEETzOUtS1I5SxCJRJPXQOPiTx0Cg8ZK
-6sUTzT1qBAnPZ/0f3F0gIQKnWe75VHDxAvumVgt0KiWbLbY62KtpVYw2gpQxxAF8
-t4E42v5rmXmsv6dPS/qlTJrSiRZzU5l2m/shqK3fAJGrajuNhMlO2D9+utrtuz/j
-GPvb6mJyiQjoX+wEqSo55Fk9nk0UpWUMTn8Av9vcWTRxK54S6yktfzrZM4sOudIm
-wZuRff8GOW2oiAJntzaQ/OqnUFUWNTN94lCmYE4NdKlX/02/FAilRJdQ+XY7upNs
-8Cy0g9PT4+y3M3FyyATOaugHszu1OAq6iQREBBgBAgAPAhsCBQJYsb3rBQkIM/K8
-AinBXSAEGQECAAYFAlRQBC8ACgkQ4gbCn78E/xc2jxAAsFu+jcO/pKvyixFilobP
-ztiftnk5NQ207eVA5rV6r/UV2ngBUn35YGpot1zltevXGs6hpS7jnvOtRgHYGjXx
-VEdG/qFbqqHMOhjbNtQYLJBusPZI+L3hImiDCuipdy6d1uPGuXbiZ+jCyRj1FsLb
-A7HRGHHFqkH6Yx7YM3FJV4VKzOw7pBrLM69fGu0WWFWqCf70ErreZudsF1EOjjpy
-UAMJO3ktKXUIRPNHSizAjgxDFW2hkkbILeBUbq6Qy5Qkt+K9tLrL08MloXSYiRnB
-/aIqDF+mKc01292KkmUSy2AFX3xcrZDdwPZCmn9u2sbQ+yxek25cqbyG0qA9ihjr
-h/j1VILNs8kXUglI6qaj40aWeBlU+kCjedII2QNjWiye+0pbtrm512VUUaGm6xZH
-yE/5iy9Xyogi4L7r1xPxEG/n2dIQJUzkTpuy3ZDZot/eXYc6rMhDGrWXBk3Vy4j/
-feYoMhaWazYZf9jOU6wCHoUHM8LhwE2rgEk65f01yIGoMba8e372N/3lPKl8LUoY
-J36k97HLtapSewPKe793e6jsOH3SGGVJRQJ3BhKfxo60xNTi31aQz0mdX4jaDiGQ
-SXAKwfjO8kEbFEunI06TcXCbBD/NMz6MnabC6CbCu76vF2vIFjXAnh7snFmmJL9/
-0FKPnHAn1OV+RVPjQJxovtgJEDgEu4LTncDj8nYP/18SgUQ7yUTgc2DjwVOlrUFg
-L5TPWtqjiRPaiUC5GwW/aFRKGyD0BcUaFFxJP2qhR5zMdmPrbZq9jTzPqPbDfq7P
-in75pRLg0LIPjDWuygqhXIRECxSBsl5OA+zPsyoZEos0sZZI88OFAQR3jXwWANA6
-hlLYoXohND92bk8s7BcQr9XJqghk93X0TX2HMJxExLKfp1jFF6gFEWpeqx91DIhQ
-yKLStxOeFCPsf8dUccg7QrgHZ60wE8W9jbDQW7kwhOlQ27ClvKKW1H/ZQTg3eFFc
-4IuI86tAAb+AJNlr7CGyf5sQfzqMdPxy9ywvg3Kk9VEK7Y1mHGVcsvqAlfyOgOMJ
-c0fZ2U/1tDEKjk18KNxGsiIRrrwdYT9irwE0hmgWc3o4lxV6zFNJ1pxcmKkRhCMt
-hNa31KCKBiZDhZMQY5fSmUmNAFEsV92Z1o29BF3rK5FmH4FLueO+39aAoZiZO3W2
-jtrSelzq6ol5XlAOB7Ol7o6p+9M1gVx4OgYaR5k/9jDgeI47MMW81rBSxG0FQeRu
-0zlB2/o12Wf5RAz/laXISIUekapwlU5FYMjoP+ziAFZMN9QAbo0MLCuvj8FqX/B7
-NpeBB+VNeou2GdK5+DPbn2sHWp1rtayNVipqMhWlEyrmsFHvkDFDcGYRSKbuSCd7
-O+PKZpCkfwcZAXF25edquQENBFY3Xw0BCADsNcfIEJ23NJ9GmIyotqT6MUggt2CL
-TAON/ats4TeI4et851NdVI1GBMVChcmNhN36wY0OS2J+lSfozyDqksgRojMFs3Bj
-vn4xEdvS7UU6UdX3TTSiy2RamLdjJjd9QA9JznOXxY/q0kIW6A9gG58D/tKt43Ad
-QdglQ5XEMG5JY11rS9bNIrDk+QsNv8bUcN6ElFTWzOXMdFyqm0Im6ZWcxhlSdgbJ
-G2HE86L/DemLdNwOXetL4V2csWz+ZTY6+S6jbk2T2gSqUSel52voVU60k/HxMckY
-UBOaUsAxPY0R1/XV3nRfhZQVfGSfMpzvrggi5Whz1uZ8fVmyTrK5H3dfABEBAAGJ
-Ah8EGAECAAkFAlY3Xw0CGwwACgkQOAS7gtOdwOMgJg/9F2B+yY0bHwja0LHJ6+Wh
-QYyRdSmis5JlAL49EPEGFCptA5mECgUF48F84ZU4tlVs9sbzmKH8U9BVPU4EECHg
-f1HftrX+EoXf8n5BejdvKygFyzevoA2IEqH3u8WkDrOLXoNXDI2CcT4Uu25cQGJB
-6lvVr8ohUZ2Q3krZzugCZtDhSclG56/rk+2MhApT3yeOcs2ED1htvdnU7OIeAxwP
-8Rcu0f8kWUoGvJ3KlbmA6DoMrvlVRheJsKDLXcUV6XGKd2o5CDuKuNbPx5MIg4i5
-FwcAR8gbNGsNVOX29g57YWZnXj7rKF0Ab2Vmx9Ir/y0qoRoSfLgH7no6io2Wx30i
-8mfq2qd/6RwHi4/KimJJs9Xle9BQWb0kGU2FCnzmc8gAXoiGdzUTdvRxtHa2l8HO
-PlI78z0cklwWr1cdnUXU87zJisqGFKvbyTNBEJmItXswDEuQcbzIsIMyDM5REdes
-H1OpSJBc/A095yy/JuT7618R4T6BaWDD0oEvBtCds36Vd2jwtTRBb5cXvcfMc1Rf
-o5lNafpO//HcKgJ/Oa4zAmKaXURzi+rBrxO+CGVpXQex+4+cohxxZ6gTbgNxYmGA
-7v556HDxJBUyytvlzGiKqg97GKsHiISy4o5BLQPYFMpCNgB3rcb5RR0v5tGeeuhr
-aXOo2lPgP440izPGh5s38Oc=
-=8iHq
------END PGP PUBLIC KEY BLOCK-----
# SPDX-License-Identifier: AGPL-3.0
FROM debian:stretch
-MAINTAINER Nico Cesar <nico@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
ENV DEBIAN_FRONTEND noninteractive
-# Install RVM
-COPY D39DC0E3.asc /tmp
+# Install dependencies
RUN apt-get update && \
- apt-get -y install --no-install-recommends curl ca-certificates gpg procps && \
- gpg --import /tmp/D39DC0E3.asc && \
+ apt-get -y install --no-install-recommends curl ca-certificates gpg procps
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3
# SPDX-License-Identifier: AGPL-3.0
FROM ubuntu:trusty
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
ENV DEBIAN_FRONTEND noninteractive
-# Install dependencies and RVM
+# Install dependencies
RUN apt-get update && \
- apt-get -y install --no-install-recommends curl ca-certificates python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip binutils build-essential ca-certificates && \
- gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+ apt-get -y install --no-install-recommends curl ca-certificates python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip binutils build-essential ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3
# SPDX-License-Identifier: AGPL-3.0
FROM ubuntu:xenial
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
ENV DEBIAN_FRONTEND noninteractive
-# Install RVM
+# Install dependencies
RUN apt-get update && \
- apt-get -y install --no-install-recommends curl ca-certificates && \
- gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+ apt-get -y install --no-install-recommends curl ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3
ENV DEBIAN_FRONTEND noninteractive
-# Install RVM
+# Install dependencies
RUN apt-get update && \
- apt-get -y install --no-install-recommends curl ca-certificates gnupg2 && \
- gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+ apt-get -y install --no-install-recommends curl ca-certificates gnupg2
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
curl -L https://get.rvm.io | bash -s stable && \
/usr/local/rvm/bin/rvm install 2.3 && \
/usr/local/rvm/bin/rvm alias create default ruby-2.3
if [[ -n "$test_packages" ]]; then
pushd "$JENKINS_DIR/package-test-dockerfiles"
+ make "$TARGET/generated"
else
pushd "$JENKINS_DIR/package-build-dockerfiles"
make "$TARGET/generated"
cd $WORKSPACE/packages/$TARGET
rm -rf "$WORKSPACE/services/nodemanager/build"
nodemanager_version=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' $WORKSPACE/services/nodemanager/arvados_node_manager.egg-info/PKG-INFO)}
-test_package_presence arvados-node-manager "$nodemanager_version" python
+iteration="${ARVADOS_BUILDING_ITERATION:-1}"
+test_package_presence arvados-node-manager "$nodemanager_version" python "$iteration"
if [[ "$?" == "0" ]]; then
- fpm_build $WORKSPACE/services/nodemanager arvados-node-manager 'Curoverse, Inc.' 'python' "$nodemanager_version" "--url=https://arvados.org" "--description=The Arvados node manager" --depends "${PYTHON2_PKG_PREFIX}-setuptools"
+ fpm_build $WORKSPACE/services/nodemanager arvados-node-manager 'Curoverse, Inc.' 'python' "$nodemanager_version" "--url=https://arvados.org" "--description=The Arvados node manager" --depends "${PYTHON2_PKG_PREFIX}-setuptools" --iteration "$iteration"
fi
# The Docker image cleaner
cd $WORKSPACE/packages/$TARGET
rm -rf "$WORKSPACE/services/dockercleaner/build"
dockercleaner_version=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' $WORKSPACE/services/dockercleaner/arvados_docker_cleaner.egg-info/PKG-INFO)}
-iteration="${ARVADOS_BUILDING_ITERATION:-3}"
+iteration="${ARVADOS_BUILDING_ITERATION:-4}"
test_package_presence arvados-docker-cleaner "$dockercleaner_version" python "$iteration"
if [[ "$?" == "0" ]]; then
fpm_build $WORKSPACE/services/dockercleaner arvados-docker-cleaner 'Curoverse, Inc.' 'python3' "$dockercleaner_version" "--url=https://arvados.org" "--description=The Arvados Docker image cleaner" --depends "${PYTHON3_PKG_PREFIX}-websocket-client = 0.37.0" --iteration "$iteration"
pip install --no-use-wheel >/dev/null 2>&1
case "$?" in
0) PIP_DOWNLOAD_SWITCHES+=(--no-use-wheel) ;;
+ 1) ;;
2) ;;
- *) echo "WARNING: `pip wheel` test returned unknown exit code $?" ;;
+ *) echo "WARNING: 'pip install --no-use-wheel' test returned unknown exit code $?" ;;
esac
while read -r line || [[ -n "$line" ]]; do
sdk/ruby
sdk/go/arvados
sdk/go/arvadosclient
+sdk/go/auth
sdk/go/dispatch
sdk/go/keepclient
sdk/go/health
which Xvfb || fatal "No xvfb. Try: apt-get install xvfb"
echo -n 'graphviz: '
dot -V || fatal "No graphviz. Try: apt-get install graphviz"
+ echo -n 'geckodriver: '
+ geckodriver --version | grep ^geckodriver || echo "No geckodriver. Try: wget -O- https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz | sudo tar -C /usr/local/bin -xzf - geckodriver"
if [[ "$NEED_SDK_R" = true ]]; then
# R SDK stuff
lib/dispatchcloud
sdk/go/arvados
sdk/go/arvadosclient
+ sdk/go/auth
sdk/go/blockdigest
sdk/go/dispatch
sdk/go/health
- user/topics/keep.html.textile.liquid
- user/topics/arv-copy.html.textile.liquid
- user/topics/storage-classes.html.textile.liquid
+ - user/topics/collection-versioning.html.textile.liquid
- Running workflows at the command line:
- user/cwl/cwl-runner.html.textile.liquid
- user/cwl/cwl-run-options.html.textile.liquid
- Upgrading and migrations:
- admin/upgrading.html.textile.liquid
- install/migrate-docker19.html.textile.liquid
+ - admin/upgrade-crunch2.html.textile.liquid
- Users and Groups:
- install/cheat_sheet.html.textile.liquid
- admin/activation.html.textile.liquid
- Cloud:
- admin/storage-classes.html.textile.liquid
- admin/spot-instances.html.textile.liquid
+ - Other:
+ - admin/collection-versioning.html.textile.liquid
installguide:
- Overview:
- install/index.html.textile.liquid
--- /dev/null
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+On the <strong>API server</strong>, use the following commands:
+
+<notextile>
+<pre><code>~$ <span class="userinput">cd /var/www/arvados-api/current</span>
+$ <span class="userinput">sudo -u <b>webserver-user</b> RAILS_ENV=production bundle exec script/create_superuser_token.rb</span>
+zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+</code></pre>
+</notextile>
<notextile>
<pre><code>~$ <span class="userinput">uuid_prefix=`arv --format=uuid user current | cut -d- -f1`</span>
-~$ <span class="userinput">project_uuid=`arv --format=uuid group create --group "{\"owner_uuid\":\"$uuid_prefix-tpzed-000000000000000\", \"name\":\"Arvados Standard Docker Images\"}"`</span>
+~$ <span class="userinput">project_uuid=`arv --format=uuid group create --group "{\"owner_uuid\":\"$uuid_prefix-tpzed-000000000000000\", \"group_class\":\"project\", \"name\":\"Arvados Standard Docker Images\"}"`</span>
~$ <span class="userinput">echo "Arvados project uuid is '$project_uuid'"</span>
~$ <span class="userinput">read -rd $'\000' newlink <<EOF; arv link create --link "$newlink"</span>
<span class="userinput">{
--- /dev/null
+---
+layout: default
+navsection: admin
+title: Configuring collection versioning
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+This page describes how to enable and configure the collection versioning feature on the API server.
+
+h3. API Server configuration
+
+There are 2 configuration settings that control this feature, both go on the @application.yml@ file.
+
+h4. Settting: @collection_versioning@ (Boolean. Default: false)
+
+If @true@, collection versioning is enabled, meaning that new version records can be created. Note that if you set @collection_versioning@ to @false@ after being enabled, old versions will still be accessible, but further changes will not be versioned.
+
+h4. Setting: @preserve_version_if_idle@ (Numeric. Default: -1)
+
+This setting control the auto-save aspect of collection versioning, and can be set to:
+* @-1@: Never auto-save versions. Only save versions when the client ask for it by setting @preserve_version@ to @true@ on any given collection.
+* @0@: Preserve all versions every time a collection gets a versionable update.
+* @N@ (being N > 0): Preserve version when a collection gets a versionable update after a period of at least N seconds since the last time it was modified.
+
+h3. Using collection versioning
+
+"Discussed in the user guide":{{site.baseurl}}/user/topics/collection-versioning.html
\ No newline at end of file
Cluster:
# The cluster uuid prefix
zzzzz:
+ ManagementToken: xyzzy
NodeProfile:
# For each node, the profile name corresponds to a
# locally-resolvable hostname, and describes which Arvados
# services are available on that machine.
api:
arvados-controller:
- Listen: 8000
+ Listen: :8000
arvados-api-server:
- Listen: 8001
+ Listen: :8001
manage:
arvados-node-manager:
- Listen: 8002
+ Listen: :8002
workbench:
arvados-workbench:
- Listen: 8003
+ Listen: :8003
arvados-ws:
- Listen: 8004
+ Listen: :8004
keep:
keep-web:
- Listen: 8005
+ Listen: :8005
keepproxy:
- Listen: 8006
+ Listen: :8006
+ keep-balance:
+ Listen: :9005
keep0:
keepstore:
- Listen: 25701
+ Listen: :25107
keep1:
keepstore:
- Listen: 25701
+ Listen: :25107
</pre>
}
</pre>
+h2. Keep-balance
+
+Keep-balance exports metrics at @/metrics@ -- e.g., @http://keep.zzzzz.arvadosapi.com:9005/metrics@.
+
+table(table table-bordered table-condensed).
+|_. Name|_. Type|_. Description|
+|arvados_keep_total_{replicas,blocks,bytes}|gauge|stored data (stored in backend volumes, whether referenced or not)|
+|arvados_keep_garbage_{replicas,blocks,bytes}|gauge|garbage data (unreferenced, and old enough to trash)|
+|arvados_keep_transient_{replicas,blocks,bytes}|gauge|transient data (unreferenced, but too new to trash)|
+|arvados_keep_overreplicated_{replicas,blocks,bytes}|gauge|overreplicated data (more replicas exist than are needed)|
+|arvados_keep_underreplicated_{replicas,blocks,bytes}|gauge|underreplicated data (fewer replicas exist than are needed)|
+|arvados_keep_lost_{replicas,blocks,bytes}|gauge|lost data (referenced by collections, but not found on any backend volume)|
+|arvados_keep_dedup_block_ratio|gauge|deduplication ratio (block references in collections ÷ distinct blocks referenced)|
+|arvados_keep_dedup_byte_ratio|gauge|deduplication ratio (block references in collections ÷ distinct blocks referenced, weighted by block size)|
+|arvados_keepbalance_get_state_seconds|summary|time to get all collections and keepstore volume indexes for one iteration|
+|arvados_keepbalance_changeset_compute_seconds|summary|time to compute changesets for one iteration|
+|arvados_keepbalance_send_pull_list_seconds|summary|time to send pull lists to all keepstore servers for one iteration|
+|arvados_keepbalance_send_trash_list_seconds|summary|time to send trash lists to all keepstore servers for one iteration|
+|arvados_keepbalance_sweep_seconds|summary|time to complete one iteration|
+
+Each @arvados_keep_@ storage state statistic above is presented as a set of three metrics:
+
+table(table table-bordered table-condensed).
+|*_blocks|distinct block hashes|
+|*_bytes|bytes stored on backend volumes|
+|*_replicas|objects/files stored on backend volumes|
+
h2. Node manager
The node manager status end point provides a snapshot of internal status at the time of the most recent wishlist update.
--- /dev/null
+---
+layout: default
+navsection: admin
+title: Upgrading to Containers API
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+The "containers" API is the recommended way to submit compute work to Arvados. It supersedes the "jobs" API, which is deprecated.
+
+h2. Benefits over the "jobs" API
+
+* Simpler and more robust execution with fewer points of failure
+* Automatic retry for containers that fail to run to completion due to infrastructure errors
+* Scales to thousands of simultaneous containers
+* Able to support alternate schedulers/dispatchers in addition to slurm
+* Improved logging, different streams logs/metrics stored in different files in the log collection
+* Records more upfront detail about the compute node, and additional metrics (such as available disk space over the course of the container run)
+* Better behavior when deciding whether to reuse past work -- pick the oldest container that matches the criteria
+* Can reuse running containers between workflows, cancelling a workflow will not cancel containers that are shared with other workflows
+* Supports setting time-to-live on intermediate output collections for automatic cleanup
+* Supports "secret" inputs, suitable for passwords or access tokens, which are hidden from the API responses and logs, and forgotten after use
+* Does not require "git" for dispatching work
+
+h2. Differences from the "jobs" API
+
+Containers cannot reuse jobs (but can reuse other containers)
+
+Uses the service "crunch-dispatch-slurm":{{site.baseurl}}/install/crunch2-slurm/install-dispatch.html instead of @crunch-dispatch.rb@
+
+Non-CWL Arvados "pipeline templates" are not supported with containers. Pipeline templates should be rewritten in CWL and registered as "Workflows".
+
+The containers APIs is incompatible with the jobs API, code which integrates with the "jobs" API must be updated to work with containers
+
+Containers have network access disabled by default
+
+The keep mount only exposes collections which are explicitly listed as inputs
+
+h2. Migrating to "containers" API
+
+Run your workflows using @arvados-cwl-runner --api=containers@ (only necessary if both the jobs and containers APIs are enabled, if the jobs API is disabled, it will use the containers API automatically)
+
+Register your workflows so they can be run from workbench using @arvados-cwl-runner --api=containers --create-workflow@
+
+Read "Migrating running CWL on jobs API to containers API":{{site.baseurl}}/user/cwl/cwl-style.html#migrate
+
+Use @arv:APIRequirement: {}@ in the @requirements@ section of your CWL file to enable network access for the container (see "Arvados CWL Extensions":{{site.baseurl}}/user/cwl/cwl-extensions.html)
+
+For examples on how to manage container requests with the Python SDK, see "Python cookbook":{{site.baseurl}}/sdk/python/cookbook.html
TODO: extract this information based on git commit messages and generate changelogs / release notes automatically.
{% endcomment %}
-h3. 2018-07-31: "#13497":https://dev.arvados.org/issues/13497 "db5107dca":https://dev.arvados.org/projects/arvados/repository/revisions/db5107dca adds a new system service, arvados-controller
-* "Install the controller":../install/install-controller.html after upgrading your system.
-* Verify your setup by confirming that API calls appear in the controller's logs (_e.g._, @journalctl -fu arvados-controller@) while loading a workbench page.
+h3. v1.2.0 (2018-09-05)
-h3. 2018-04-05: v1.1.4 regression in arvados-cwl-runner for workflows that rely on implicit discovery of secondaryFiles
+h4. Regenerate Postgres table statistics
-h4. Secondary files missing from toplevel workflow inputs
+It is recommended to regenerate the table statistics for Postgres after upgrading to v1.2.0. If autovacuum is enabled on your installation, this script would do the trick:
+
+<pre>
+#!/bin/bash
+
+set -e
+set -u
+
+tables=`echo "\dt" | psql arvados_production | grep public|awk -e '{print $3}'`
+
+for t in $tables; do
+ echo "echo 'analyze $t' | psql arvados_production"
+ time echo "analyze $t" | psql arvados_production
+done
+</pre>
+
+If you also need to do the vacuum, you could adapt the script to run 'vacuum analyze' instead of 'analyze'.
+
+h4. New component: arvados-controller
+
+Commit "db5107dca":https://dev.arvados.org/projects/arvados/repository/revisions/db5107dca adds a new system service, arvados-controller. More detail is available in story "#13496":https://dev.arvados.org/issues/13497.
+
+To add the Arvados Controller to your system please refer to the "installation instructions":../install/install-controller.html after upgrading your system to 1.2.0.
+
+Verify your setup by confirming that API calls appear in the controller's logs (_e.g._, @journalctl -fu arvados-controller@) while loading a workbench page.
+
+h3. v1.1.4 (2018-04-10)
+
+h4. arvados-cwl-runner regressions (2018-04-05)
+
+<strong>Secondary files missing from toplevel workflow inputs</strong>
+
+This only affects workflows that rely on implicit discovery of secondaryFiles.
If a workflow input does not declare @secondaryFiles@ corresponding to the @secondaryFiles@ of workflow steps which use the input, the workflow would inconsistently succeed or fail depending on whether the input values were specified as local files or referenced an existing collection (and whether the existing collection contained the secondary files or not). To ensure consistent behavior, the workflow is now required to declare in the top level workflow inputs any secondaryFiles that are expected by workflow steps.
</code></pre>
</notextile>
-h4. Secondary files on default file inputs
+This bug has been fixed in Arvados release v1.2.0.
+
+<strong>Secondary files on default file inputs</strong>
-Due to a bug in Arvados v1.1.4, @File@ inputs that have default values and also expect @secondaryFiles@ and will fail to upload default @secondaryFiles@. As an example, the following case will fail:
+@File@ inputs that have default values and also expect @secondaryFiles@ and will fail to upload default @secondaryFiles@. As an example, the following case will fail:
<pre>
class: CommandLineTool
</code></pre>
</notextile>
-This bug will be fixed in an upcoming release of Arvados.
+This bug has been fixed in Arvados release v1.2.0.
+
+h3. v1.1.3 (2018-02-08)
+
+There are no special upgrade notes for this release.
+
+h3. v1.1.2 (2017-12-22)
+
+h4. The minimum version for Postgres is now 9.4 (2017-12-08)
+
+As part of story "#11908":https://dev.arvados.org/issues/11908, commit "8f987a9271":https://dev.arvados.org/projects/arvados/repository/revisions/8f987a9271 introduces a dependency on Postgres 9.4. Previously, Arvados required Postgres 9.3.
-h3. 2017-12-08: "#11908":https://dev.arvados.org/issues/11908 "8f987a9271":https://dev.arvados.org/projects/arvados/repository/revisions/8f987a9271 now requires minimum of Postgres 9.4 (previously 9.3)
* Debian 8 (pg 9.4) and Debian 9 (pg 9.6) do not require an upgrade
* Ubuntu 16.04 (pg 9.5) does not require an upgrade
* Ubuntu 14.04 (pg 9.3) requires upgrade to Postgres 9.4: https://www.postgresql.org/download/linux/ubuntu/
*# Install the @rh-postgresql94@ backport package from either Software Collections: http://doc.arvados.org/install/install-postgresql.html or the Postgres developers: https://www.postgresql.org/download/linux/redhat/
*# Restore from the backup using @psql@
-h3. 2017-09-25: "#12032":https://dev.arvados.org/issues/12032 "68bdf4cbb":https://dev.arvados.org/projects/arvados/repository/revisions/68bdf4cbb now requires minimum of Postgres 9.3 (previously 9.1)
+h3. v1.1.1 (2017-11-30)
+
+There are no special upgrade notes for this release.
+
+h3. v1.1.0 (2017-10-24)
+
+h4. The minimum version for Postgres is now 9.3 (2017-09-25)
+
+As part of story "#12032":https://dev.arvados.org/issues/12032, commit "68bdf4cbb1":https://dev.arvados.org/projects/arvados/repository/revisions/68bdf4cbb1 introduces a dependency on Postgres 9.3. Previously, Arvados required Postgres 9.1.
+
* Debian 8 (pg 9.4) and Debian 9 (pg 9.6) do not require an upgrade
* Ubuntu 16.04 (pg 9.5) does not require an upgrade
* Ubuntu 14.04 (pg 9.3) is compatible, however upgrading to Postgres 9.4 is recommended: https://www.postgresql.org/download/linux/ubuntu/
*# Install the @rh-postgresql94@ backport package from either Software Collections: http://doc.arvados.org/install/install-postgresql.html or the Postgres developers: https://www.postgresql.org/download/linux/redhat/
*# Restore from the backup using @psql@
-h3. 2017-06-30: "#11807":https://dev.arvados.org/issues/11807 "55aafbb":https://dev.arvados.org/projects/arvados/repository/revisions/55aafbb converts old "jobs" database records from YAML to JSON, making the upgrade process slower than usual.
+h3. Older versions
+
+h4. Upgrade slower than usual (2017-06-30)
+
+As part of story "#11807":https://dev.arvados.org/issues/11807, commit "55aafbb":https://dev.arvados.org/projects/arvados/repository/revisions/55aafbb converts old "jobs" database records from YAML to JSON, making the upgrade process slower than usual.
+
* The migration can take some time if your database contains a substantial number of YAML-serialized rows (i.e., you installed Arvados before March 3, 2017 "660a614":https://dev.arvados.org/projects/arvados/repository/revisions/660a614 and used the jobs/pipelines APIs). Otherwise, the upgrade will be no slower than usual.
* The conversion runs as a database migration, i.e., during the deb/rpm package upgrade process, while your API server is unavailable.
* Expect it to take about 1 minute per 20K jobs that have ever been created/run.
-h3. 2017-06-05: "#9005":https://dev.arvados.org/issues/9005 "cb230b0":https://dev.arvados.org/projects/arvados/repository/revisions/cb230b0 reduces service discovery overhead in keep-web requests.
+h4. Service discovery overhead change in keep-web (2017-06-05)
+
+As part of story "#9005":https://dev.arvados.org/issues/9005, commit "cb230b0":https://dev.arvados.org/projects/arvados/repository/revisions/cb230b0 reduces service discovery overhead in keep-web requests.
+
* When upgrading keep-web _or keepproxy_ to/past this version, make sure to update API server as well. Otherwise, a bad token in a request can cause keep-web to fail future requests until either keep-web restarts or API server gets upgraded.
-h3. 2017-04-12: "#11349":https://dev.arvados.org/issues/11349 "2c094e2":https://dev.arvados.org/projects/arvados/repository/revisions/2c094e2 adds a "management" http server to nodemanager.
+h4. Node manager now has an http endpoint for management (2017-04-12)
+
+As part of story "#11349":https://dev.arvados.org/issues/11349, commit "2c094e2":https://dev.arvados.org/projects/arvados/repository/revisions/2c094e2 adds a "management" http server to nodemanager.
+
* To enable it, add to your configuration file: <pre>[Manage]
address = 127.0.0.1
port = 8989</pre> (see example configuration files in source:services/nodemanager/doc or https://doc.arvados.org/install/install-nodemanager.html for more info)
* The server responds to @http://{address}:{port}/status.json@ with a summary of how many nodes are in each state (booting, busy, shutdown, etc.)
-h3. 2017-03-23: "#10766":https://dev.arvados.org/issues/10766 "e8cc0d7":https://dev.arvados.org/projects/arvados/repository/revisions/e8cc0d7 replaces puma with arvados-ws as the recommended websocket server.
+h4. New websockets component (2017-03-23)
+
+As part of story "#10766":https://dev.arvados.org/issues/10766, commit "e8cc0d7":https://dev.arvados.org/projects/arvados/repository/revisions/e8cc0d7 replaces puma with arvados-ws as the recommended websocket server.
* See http://doc.arvados.org/install/install-ws.html for install/upgrade instructions.
* Remove the old puma server after the upgrade is complete. Example, with runit: <pre>
$ sudo sv down /etc/sv/puma
$ systemctl stop puma
</pre>
-h3. 2017-03-06: "#11168":https://dev.arvados.org/issues/11168 "660a614":https://dev.arvados.org/projects/arvados/repository/revisions/660a614 uses JSON instead of YAML to encode hashes and arrays in the database.
+h4. Change of database encoding for hashes and arrays (2017-03-06)
+
+As part of story "#11168":https://dev.arvados.org/issues/11168, commit "660a614":https://dev.arvados.org/projects/arvados/repository/revisions/660a614 uses JSON instead of YAML to encode hashes and arrays in the database.
+
* Aside from a slight performance improvement, this should have no externally visible effect.
* Downgrading past this version is not supported, and is likely to cause errors. If this happens, the solution is to upgrade past this version.
* After upgrading, make sure to restart puma and crunch-dispatch-* processes.
-h3. 2017-02-03: "#10969":https://dev.arvados.org/issues/10969 "74a9dec":https://dev.arvados.org/projects/arvados/repository/revisions/74a9dec introduces a Docker image format compatibility check: the @arv keep docker@ command prevents users from inadvertently saving docker images that compute nodes won't be able to run.
+h4. Docker image format compatibility check (2017-02-03)
+
+As part of story "#10969":https://dev.arvados.org/issues/10969, commit "74a9dec":https://dev.arvados.org/projects/arvados/repository/revisions/74a9dec introduces a Docker image format compatibility check: the @arv keep docker@ command prevents users from inadvertently saving docker images that compute nodes won't be able to run.
* If your compute nodes run a version of *docker older than 1.10* you must override the default by adding to your API server configuration (@/etc/arvados/api/application.yml@): <pre><code class="yaml">docker_image_formats: ["v1"]</code></pre>
* Refer to the comments above @docker_image_formats@ in @/var/www/arvados-api/current/config/application.default.yml@ or source:services/api/config/application.default.yml or issue "#10969":https://dev.arvados.org/issues/10969 for more detail.
* *NOTE:* This does *not* include any support for migrating existing Docker images from v1 to v2 format. This will come later: for now, sites running Docker 1.9 or earlier should still *avoid upgrading Docker further than 1.9.*
-h3. 2016-09-27: several Debian and RPM packages -- keep-balance ("d9eec0b":https://dev.arvados.org/projects/arvados/repository/revisions/d9eec0b), keep-web ("3399e63":https://dev.arvados.org/projects/arvados/repository/revisions/3399e63), keepproxy ("6de67b6":https://dev.arvados.org/projects/arvados/repository/revisions/6de67b6), and arvados-git-httpd ("9e27ddf":https://dev.arvados.org/projects/arvados/repository/revisions/9e27ddf) -- now enable their respective components using systemd. These components prefer YAML configuration files over command line flags ("3bbe1cd":https://dev.arvados.org/projects/arvados/repository/revisions/3bbe1cd).
+h4. Debian and RPM packages now have systemd unit files (2016-09-27)
+
+Several Debian and RPM packages -- keep-balance ("d9eec0b":https://dev.arvados.org/projects/arvados/repository/revisions/d9eec0b), keep-web ("3399e63":https://dev.arvados.org/projects/arvados/repository/revisions/3399e63), keepproxy ("6de67b6":https://dev.arvados.org/projects/arvados/repository/revisions/6de67b6), and arvados-git-httpd ("9e27ddf":https://dev.arvados.org/projects/arvados/repository/revisions/9e27ddf) -- now enable their respective components using systemd. These components prefer YAML configuration files over command line flags ("3bbe1cd":https://dev.arvados.org/projects/arvados/repository/revisions/3bbe1cd).
+
* On Debian-based systems using systemd, services are enabled automatically when packages are installed.
* On RedHat-based systems using systemd, unit files are installed but services must be enabled explicitly: e.g., <code>"sudo systemctl enable keep-web; sudo systemctl start keep-web"</code>.
* The new systemd-supervised services will not start up successfully until configuration files are installed in /etc/arvados/: e.g., <code>"Sep 26 18:23:55 62751f5bb946 keep-web[74]: 2016/09/26 18:23:55 open /etc/arvados/keep-web/keep-web.yml: no such file or directory"</code>
** keepproxy - /etc/arvados/keepproxy/keepproxy.yml
** arvados-git-httpd - /etc/arvados/arv-git-httpd/arv-git-httpd.yml
-h3. 2016-05-31: "ae72b172c8":https://dev.arvados.org/projects/arvados/repository/revisions/ae72b172c8 and "3aae316c25":https://dev.arvados.org/projects/arvados/repository/revisions/3aae316c25 install Python modules and scripts to different locations on the filesystem.
+h4. Installation paths for Python modules and script changed (2016-05-31)
+
+Commits "ae72b172c8":https://dev.arvados.org/projects/arvados/repository/revisions/ae72b172c8 and "3aae316c25":https://dev.arvados.org/projects/arvados/repository/revisions/3aae316c25 change the filesystem location where Python modules and scripts are installed.
+
* Previous packages installed these files to the distribution's preferred path under @/usr/local@ (or the equivalent location in a Software Collection). Now they get installed to a path under @/usr@. This improves compatibility with other Python packages provided by the distribution. See "#9242":https://dev.arvados.org/issues/9242 for more background.
* If you simply import Python modules from scripts, or call Python tools relying on $PATH, you don't need to make any changes. If you have hardcoded full paths to some of these files (e.g., in symbolic links or configuration files), you will need to update those paths after this upgrade.
-h3. 2016-04-25: "eebcb5e":https://dev.arvados.org/projects/arvados/repository/revisions/eebcb5e requires the crunchrunner package to be installed on compute nodes and shell nodes in order to run CWL workflows.
+h4. Crunchrunner package is required on compute and shell nodes (2016-04-25)
+
+Commit "eebcb5e":https://dev.arvados.org/projects/arvados/repository/revisions/eebcb5e requires the crunchrunner package to be installed on compute nodes and shell nodes in order to run CWL workflows.
+
* On each Debian-based compute node and shell node, run: @sudo apt-get install crunchrunner@
* On each Red Hat-based compute node and shell node, run: @sudo yum install crunchrunner@
-h3. 2016-04-21: "3c88abd":https://dev.arvados.org/projects/arvados/repository/revisions/3c88abd changes the Keep permission signature algorithm.
+h4. Keep permission signature algorithm change (2016-04-21)
+
+Commit "3c88abd":https://dev.arvados.org/projects/arvados/repository/revisions/3c88abd changes the Keep permission signature algorithm.
+
* All software components that generate signatures must be upgraded together. These are: keepstore, API server, keep-block-check, and keep-rsync. For example, if keepstore < 0.1.20160421183420 but API server >= 0.1.20160421183420, clients will not be able to read or write data in Keep.
* Jobs and client operations that are in progress during the upgrade (including arv-put's "resume cache") will fail.
-h3. 2015-01-05: "e1276d6e":https://dev.arvados.org/projects/arvados/repository/revisions/e1276d6e disables Workbench's "Getting Started" popup by default.
+h4. Workbench's "Getting Started" popup disabled by default (2015-01-05)
+
+Commit "e1276d6e":https://dev.arvados.org/projects/arvados/repository/revisions/e1276d6e disables Workbench's "Getting Started" popup by default.
+
* If you want new users to continue seeing this popup, set @enable_getting_started_popup: true@ in Workbench's @application.yml@ configuration.
-h3. 2015-12-03: "5590c9ac":https://dev.arvados.org/projects/arvados/repository/revisions/5590c9ac makes a Keep-backed writable scratch directory available in crunch jobs (see "#7751":https://dev.arvados.org/issues/7751)
+h4. Crunch jobs now have access to Keep-backed writable scratch storage (2015-12-03)
+
+Commit "5590c9ac":https://dev.arvados.org/projects/arvados/repository/revisions/5590c9ac makes a Keep-backed writable scratch directory available in crunch jobs (see "#7751":https://dev.arvados.org/issues/7751)
+
* All compute nodes must be upgraded to arvados-fuse >= 0.1.2015112518060 because crunch-job uses some new arv-mount flags (--mount-tmp, --mount-by-pdh) introduced in merge "346a558":https://dev.arvados.org/projects/arvados/repository/revisions/346a558
* Jobs will fail if the API server (in particular crunch-job from the arvados-cli gem) is upgraded without upgrading arvados-fuse on compute nodes.
-h3. 2015-11-11: "1e2ace5":https://dev.arvados.org/projects/arvados/repository/revisions/1e2ace5 changes recommended config for keep-web (see "#5824":https://dev.arvados.org/issues/5824)
+h4. Recommended configuration change for keep-web (2015-11-11)
+
+Commit "1e2ace5":https://dev.arvados.org/projects/arvados/repository/revisions/1e2ace5 changes recommended config for keep-web (see "#5824":https://dev.arvados.org/issues/5824)
+
* proxy/dns/ssl config should be updated to route "https://download.uuid_prefix.arvadosapi.com/" requests to keep-web (alongside the existing "collections" routing)
* keep-web command line adds @-attachment-only-host download.uuid_prefix.arvadosapi.com@
* Workbench config adds @keep_web_download_url@
* More info on the (still beta/non-TOC-linked) "keep-web doc page":http://doc.arvados.org/install/install-keep-web.html
-h3. 2015-11-04: "1d1c6de":https://dev.arvados.org/projects/arvados/repository/revisions/1d1c6de removes stopped containers (see "#7444":https://dev.arvados.org/issues/7444)
+h4. Stopped containers are now automatically removed on compute nodes (2015-11-04)
+
+Commit "1d1c6de":https://dev.arvados.org/projects/arvados/repository/revisions/1d1c6de removes stopped containers (see "#7444":https://dev.arvados.org/issues/7444)
+
* arvados-docker-cleaner removes _all_ docker containers as soon as they exit, effectively making @docker run@ default to @--rm@. If you run arvados-docker-cleaner on a host that does anything other than run crunch-jobs, and you still want to be able to use @docker start@, read the "new doc page":http://doc.arvados.org/install/install-compute-node.html to learn how to turn this off before upgrading.
-h3. 2015-11-04: "21006cf":https://dev.arvados.org/projects/arvados/repository/revisions/21006cf adds a keep-web service (see "#5824":https://dev.arvados.org/issues/5824)
-* Nothing relies on it yet, but early adopters can install it now by following http://doc.arvados.org/install/install-keep-web.html (it is not yet linked in the TOC).
+h4. New keep-web service (2015-11-04)
+
+Commit "21006cf":https://dev.arvados.org/projects/arvados/repository/revisions/21006cf adds a new keep-web service (see "#5824":https://dev.arvados.org/issues/5824).
+
+* Nothing relies on keep-web yet, but early adopters can install it now by following http://doc.arvados.org/install/install-keep-web.html (it is not yet linked in the TOC).
|replication_confirmed_at|datetime|When replication_confirmed was confirmed. If replication_confirmed is null, this field is also null.||
|trash_at|datetime|If @trash_at@ is non-null and in the past, this collection will be hidden from API calls. May be untrashed.||
|delete_at|datetime|If @delete_at@ is non-null and in the past, the collection may be permanently deleted.||
-|is_trashed|datetime|True if @trash_at@ is in the past, false if not.||
+|is_trashed|boolean|True if @trash_at@ is in the past, false if not.||
+|current_version_uuid|string|UUID of the collection's current version. On new collections, it'll be equal to the @uuid@ attribute.||
+|version|number|Version number, starting at 1 on new collections. This attribute is read-only.||
+|preserve_version|boolean|When set to true on a current version, it will be saved on the next versionable update.||
h3. Conditions of creating a Collection
h3. delete
-Put a Collection in the trash. This sets the @trash_at@ field to @now@ and @delete_at@ field to @now@ + token TTL. A trashed group is invisible to most API calls unless the @include_trash@ parameter is true.
+Put a Collection in the trash. This sets the @trash_at@ field to @now@ and @delete_at@ field to @now@ + token TTL. A trashed collection is invisible to most API calls unless the @include_trash@ parameter is true.
Arguments:
See "common resource list method.":{{site.baseurl}}/api/methods.html#index
+table(table table-bordered table-condensed).
+|_. Argument |_. Type |_. Description |_. Location |_. Example |
+|include_trash|boolean (default false)|Include trashed collections.|query||
+|include_old_versions|boolean (default false)|Include past versions of the collection(s) being listed, if any.|query||
+
Note: Because adding access tokens to manifests can be computationally expensive, the @manifest_text@ field is not included in results by default. If you need it, pass a @select@ parameter that includes @manifest_text@.
h3. update
|container_uuid|string|The uuid of the container that satisfies this container_request. The system may return a preexisting Container that matches the container request criteria. See "Container reuse":#container_reuse for more details.|Container reuse is the default behavior, but may be disabled with @use_existing: false@ to always create a new container.|
|container_count_max|integer|Maximum number of containers to start, i.e., the maximum number of "attempts" to be made.||
|mounts|hash|Objects to attach to the container's filesystem and stdin/stdout.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Objects to attach to the container's filesystem. Only "json" or "text" mount types allowed.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
|runtime_constraints|hash|Restrict the container's access to compute resources and the outside world.|Required when in "Committed" state. e.g.,<pre><code>{
"ram":12000000000,
"vcpus":2,
|log_uuid|string|Log collection containing log messages provided by the scheduler and crunch processes.|Null if the container has not yet completed.|
|output_uuid|string|Output collection created when the container finished successfully.|Null if the container has failed or not yet completed.|
|filters|string|Additional constraints for satisfying the container_request, given in the same form as the filters parameter accepted by the container_requests.list API.|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc. |Not returned in API responses. Reset to null when state is "Complete" or "Cancelled".|
+|runtime_user_uuid|string|The user permission that will be granted to this container.||
+|runtime_auth_scopes|array of string|The scopes associated with the auth token used to run this container.||
h2(#priority). Priority
h2(#container_reuse). Container reuse
-When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, and runtime_constraints being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints are compared without regard to key order.)
+When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, secret_mounts, runtime_constraints, runtime_user_uuid, and runtime_auth_scopes being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints use normalized key order.)
In order of preference, the system will use:
* The first matching container to have finished successfully (i.e., reached state "Complete" with an exit_code of 0) whose log and output collections are still available.
|command|array of strings|Command to execute.| Must be equal to a ContainerRequest's command in order to satisfy the ContainerRequest.|
|output_path|string|Path to a directory or file inside the container that should be preserved as this container's output when it finishes.|Must be equal to a ContainerRequest's output_path in order to satisfy the ContainerRequest.|
|mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
|runtime_constraints|hash|Compute resources, and access to the outside world, that are / were available to the container.
Generally this will contain additional keys that are not present in any corresponding ContainerRequests: for example, even if no ContainerRequests specified constraints on the number of CPU cores, the number of cores actually used will be recorded here.|e.g.,
<pre><code>{
|progress|number|A number between 0.0 and 1.0 describing the fraction of work done.||
|priority|integer|Range 0-1000. Indicate scheduling order preference.|Currently assigned by the system as the max() of the priorities of all associated ContainerRequests. See "container request priority":container_requests.html#priority .|
|exit_code|integer|Process exit code.|Null if state!="Complete"|
-|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.|Null if state∉{"Locked","Running"}|
+|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc. Automatically assigned.|Null if state∉{"Locked","Running"} or if @runtime_token@ was provided.|
|locked_by_uuid|string|UUID of a token, indicating which dispatch process changed state to Locked. If null, any token can be used to lock. If not null, only the indicated token can modify this container.|Null if state∉{"Locked","Running"}|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.|Not returned in API responses. Reset to null when state is "Complete" or "Cancelled".|
h2(#container_states). Container states
h2. Create a dispatcher token
-Create an Arvados superuser token for use by the dispatcher. If you have multiple dispatch processes, you should give each one a different token. *On the API server*, run:
+Create an Arvados superuser token for use by the dispatcher. If you have multiple dispatch processes, you should give each one a different token.
-<notextile>
-<pre><code>apiserver:~$ <span class="userinput">cd /var/www/arvados-api/current</span>
-apiserver:/var/www/arvados-api/current$ <span class="userinput">sudo -u <b>webserver-user</b> RAILS_ENV=production bundle exec script/create_superuser_token.rb</span>
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
-</code></pre>
-</notextile>
+{% include 'create_superuser_token' %}
h2. Configure the dispatcher
h3. Create a keep-balance token
-Create an Arvados superuser token for use by keep-balance. *On the API server*, run:
+Create an Arvados superuser token for use by keep-balance.
{% include 'create_superuser_token' %}
On the host running keep-balance, create @/etc/arvados/keep-balance/keep-balance.yml@ using the token you generated above. Follow this YAML format:
<notextile>
-<pre><code>Client:
+<pre><code>Listen: :9005
+Client:
APIHost: <span class="userinput">uuid_prefix.your.domain</span>:443
AuthToken: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
KeepServiceTypes:
- disk
+Listen: :9005
+ManagementToken: <span class="userinput">xyzzy</span>
RunPeriod: 10m
CollectionBatchSize: 100000
CollectionBuffers: 1000
The API server needs to be informed about the presence of your Keepproxy server.
-First, if you don't already have an admin token, create a superuser token:
+First, if you don't already have an admin token, create a superuser token.
{% include 'create_superuser_token' %}
# Format of request/response and error logs: "json" or "text".
LogFormat: json
-# The secret key that must be provided by monitoring services
-# wishing to access the health check endpoint (/_health).
-ManagementToken: ""
+# The secret key that must be provided by monitoring services when
+# using the health check and metrics endpoints (/_health, /metrics).
+ManagementToken: xyzzy
# Maximum RAM to use for data buffers, given in multiples of block
# size (64 MiB). When this limit is reached, HTTP requests requiring
The API server needs to be informed about the presence of your Keepstore servers.
-First, if you don't already have an admin token, create a superuser token:
+First, if you don't already have an admin token, create a superuser token.
{% include 'create_superuser_token' %}
<notextile>
<pre>
-<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>your.shell.server.hostname</b>"}'</span>
+<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>your.shell.server.hostname.without.domain</b>"}'</span>
zzzzz-2x53u-zzzzzzzzzzzzzzz</code>
</pre>
</notextile>
h2(#migrate). Migrating running CWL on jobs API to containers API
-* When migrating from jobs API (--api=jobs) (sometimes referred to as "crunch v1") to the containers API (--api=containers) ("crunch v2") there are a few differences in behavior:
-** The tool is limited to accessing only collections which are explicitly listed in the input, and further limited to only the subdirectories of collections listed in input. For example, given an explicit file input @/dir/subdir/file1.txt@, a tool will not be able to implicitly access the file @/dir/file2.txt@. Use @secondaryFiles@ or a @Directory@ input to describe trees of files.
-** Files listed in @InitialWorkDirRequirement@ appear in the output directory as normal files (not symlinks) but cannot be moved, renamed or deleted. These files will be added to the output collection but without any additional copies of the underlying data.
-** Tools are disallowed network access by default. Tools which require network access must include @arv:APIRequirement: {}@ in their @requirements@ section.
+When migrating from jobs API (--api=jobs) (sometimes referred to as "crunch v1") to the containers API (--api=containers) ("crunch v2") there are a few differences in behavior:
+
+* A tool may fail to find an input file that could be found when run under the jobs API. This is because tools are limited to accessing collections explicitly listed in the input, and further limited to those individual files or subdirectories that are listed. For example, given an explicit file input @/dir/subdir/file1.txt@, a tool will not be allowed to implicitly access a file in the parent directory @/dir/file2.txt@. Use @secondaryFiles@ or a @Directory@ for files that need to be grouped together.
+* A tool may fail when attempting to rename or delete a file in the output directory. This may happen because files listed in @InitialWorkDirRequirement@ appear in the output directory as normal files (not symlinks) but cannot be moved, renamed or deleted unless marked as "writable" in CWL. These files will be added to the output collection but without any additional copies of the underlying data.
+* A tool may fail when attempting to access the network. This may happen because, unlike the jobs API, under the containers API network access is disabled by default. Tools which require network access should add @arv:APIRequirement: {}@ to the @requirements@ section.
--- /dev/null
+---
+layout: default
+navsection: userguide
+title: Using collection versioning
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+When collection versioning is enabled, updating certain collection attributes (@name@, @description@, @properties@, @manifest_text@) will save a copy of the collection state, previous to the update. This copy (a new collection record) will have its own @uuid@, and a @current_version_uuid@ attribute pointing to the current version's @uuid@.
+
+Every collection has a @version@ attribute that indicates its version number, starting from 1 on new collections and incrementing by 1 with every versionable update. All collections point to their most current version via the @current_version_uuid@ attribute, being @uuid@ and @current_version_uuid@ equal on those collection records that are the the current version of themselves. Note that the "current version" collection record doesn't change its @uuid@, "past versions" are saved as new records every time it's needed, pointing to the current collection record.
+
+A version will be saved when one of the following conditions is true:
+
+One is by "configuring (system-wide) the collection's idle time":{{site.baseurl}}/admin/collection-versioning.html. This idle time is checked against the @modified_at@ attribute so that the version is saved when one or more of the previously enumerated attributes get updated and the @modified_at@ is at least at the configured idle time in the past. This way, a frequently updated collection won't create lots of version records that may not be useful.
+
+The other way to trigger a version save, is by setting @preserve_version@ to @true@ on the current version collection record: this ensures that the current state will be preserved as a version the next time it gets updated.
+
+h3. Collection's past versions behavior & limitations
+
+Past version collection records are read-only, if you need to make changes to one of them, the suggested approach is to copy it into a new collection before updating.
+
+Some attributes are automatically synced when they change on the current version: @owner_uuid@, @delete_at@, @trash_at@, @is_trashed@, @replication_desired@ and @storage_classes_desired@. This way, old versions follow the current one on several configurations. In the special case that a current version's @uuid@ gets updated, their past versions get also updated to point to the newer UUID. When a collection is deleted, any past versions are deleted along with it.
+
+Permissions on past versions are the same as their current version, the system does not allow attaching permission links to old versions. If you need to give special access to someone to a particular old version, the correct procedure is by copying it as a new collection.
+
+h3. Example: Accessing past versions of a collection
+
+To request a particular collection with all its versions you should request a list filtering the current version's UUID and passing the @include_old_versions@ query parameter. For example, using the @arv@ command line client:
+
+<pre>
+$ arv collection index --filters '[["current_version_uuid", "=", "o967z-4zz18-ynmlhyjbg1arnr2"]]' --include-old-versions
+{
+ "items":[
+ {
+ "uuid":"o967z-4zz18-i3ucessyo6xxadt",
+ "created_at":"2018-10-05T14:43:38.916885000Z",
+ "modified_at":"2018-10-05T14:44:31.098019000Z",
+ "version":1,
+ "current_version_uuid":"o967z-4zz18-ynmlhyjbg1arnr2"
+ },
+ {
+ "uuid":"o967z-4zz18-ynmlhyjbg1arnr2",
+ "created_at":"2018-10-05T14:43:38.916885000Z",
+ "modified_at":"2018-10-05T14:44:31.078643000Z",
+ "version":2,
+ "current_version_uuid":"o967z-4zz18-ynmlhyjbg1arnr2"
+ }
+ ],
+ "items_available":2
+}
+</pre>
+
+To access a specific collection version using filters:
+
+<pre>
+$ arv collection index --filters '[["current_version_uuid", "=", "o967z-4zz18-ynmlhyjbg1arnr2"], ["version", "=", 1]]' --include-old-versions
+{
+ "items":[
+ {
+ "uuid":"o967z-4zz18-i3ucessyo6xxadt",
+ "created_at":"2018-10-05T14:43:38.916885000Z",
+ "modified_at":"2018-10-05T14:44:31.098019000Z",
+ "version":1,
+ "current_version_uuid":"o967z-4zz18-ynmlhyjbg1arnr2"
+ }
+ ],
+ "items_available":1
+}
+</pre>
+
+You can also access it directly via a GET request using its UUID:
+
+<pre>
+$ arv collection get --uuid o967z-4zz18-i3ucessyo6xxadt
+{
+ "uuid":"o967z-4zz18-i3ucessyo6xxadt",
+ "created_at":"2018-10-05T14:43:38.916885000Z",
+ "modified_at":"2018-10-05T14:44:31.098019000Z",
+ "version":1,
+ "current_version_uuid":"o967z-4zz18-ynmlhyjbg1arnr2"
+}
+</pre>
+
+h3. Example: Ensuring a version is preserved
+
+As stated before, regardless of the collection's auto-save idle time cluster configuration, the user has the ability to request that a particular collection state should be preserved.
+
+When working on a collection, if there's a need to preserve the current state as a new version, the @preserve_version@ attribute should be set to @true@. This will trigger a new version creation on the next update, keeping this "version 2" state as a snapshot.
+
+<pre>
+$ arv collection update --uuid o967z-4zz18-ynmlhyjbg1arnr2 -c '{"preserve_version":true}'
+{
+ "uuid":"o967z-4zz18-ynmlhyjbg1arnr2",
+ "created_at":"2018-10-05T14:43:38.916885000Z",
+ "modified_at":"2018-10-05T15:12:57.986454000Z",
+ "version":2,
+ "current_version_uuid":"o967z-4zz18-ynmlhyjbg1arnr2",
+ "preserve_version":true
+}
+</pre>
+
+Once the @preserve_version@ attribute is set to @true@, it cannot be changed to @false@ and it will only be reset when a versionable update on the collection triggers a version save.
type NodeProfile struct {
Controller SystemServiceInstance `json:"arvados-controller"`
Health SystemServiceInstance `json:"arvados-health"`
+ Keepbalance SystemServiceInstance `json:"keep-balance"`
Keepproxy SystemServiceInstance `json:"keepproxy"`
Keepstore SystemServiceInstance `json:"keepstore"`
Keepweb SystemServiceInstance `json:"keep-web"`
ServiceNameNodemanager ServiceName = "arvados-node-manager"
ServiceNameWorkbench ServiceName = "arvados-workbench"
ServiceNameWebsocket ServiceName = "arvados-ws"
+ ServiceNameKeepbalance ServiceName = "keep-balance"
ServiceNameKeepweb ServiceName = "keep-web"
ServiceNameKeepproxy ServiceName = "keepproxy"
ServiceNameKeepstore ServiceName = "keepstore"
ServiceNameNodemanager: np.Nodemanager.Listen,
ServiceNameWorkbench: np.Workbench.Listen,
ServiceNameWebsocket: np.Websocket.Listen,
+ ServiceNameKeepbalance: np.Keepbalance.Listen,
ServiceNameKeepweb: np.Keepweb.Listen,
ServiceNameKeepproxy: np.Keepproxy.Listen,
ServiceNameKeepstore: np.Keepstore.Listen,
// ResourceListParams expresses which results are requested in a
// list/index API.
type ResourceListParams struct {
- Select []string `json:"select,omitempty"`
- Filters []Filter `json:"filters,omitempty"`
- IncludeTrash bool `json:"include_trash,omitempty"`
- Limit *int `json:"limit,omitempty"`
- Offset int `json:"offset,omitempty"`
- Order string `json:"order,omitempty"`
- Distinct bool `json:"distinct,omitempty"`
- Count string `json:"count,omitempty"`
+ Select []string `json:"select,omitempty"`
+ Filters []Filter `json:"filters,omitempty"`
+ IncludeTrash bool `json:"include_trash,omitempty"`
+ IncludeOldVersions bool `json:"include_old_versions,omitempty"`
+ Limit *int `json:"limit,omitempty"`
+ Offset int `json:"offset,omitempty"`
+ Order string `json:"order,omitempty"`
+ Distinct bool `json:"distinct,omitempty"`
+ Count string `json:"count,omitempty"`
}
// A Filter restricts the set of records returned by a list/index API.
NonexistentCollection = "zzzzz-4zz18-totallynotexist"
HelloWorldCollection = "zzzzz-4zz18-4en62shvi99lxd4"
FooBarDirCollection = "zzzzz-4zz18-foonbarfilesdir"
+ WazVersion1Collection = "zzzzz-4zz18-25k12570yk1ver1"
UserAgreementPDH = "b519d9cb706a29fc7ea24dbea2f05851+93"
FooPdh = "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"
HelloWorldPdh = "55713e6a34081eb03609e7ad5fcad129+62"
return &Credentials{Tokens: []string{}}
}
-func NewCredentialsFromHTTPRequest(r *http.Request) *Credentials {
+func CredentialsFromRequest(r *http.Request) *Credentials {
+ if c, ok := r.Context().Value(contextKeyCredentials).(*Credentials); ok {
+ // preloaded by middleware
+ return c
+ }
c := NewCredentials()
c.LoadTokensFromHTTPRequest(r)
return c
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+ "context"
+ "net/http"
+)
+
+type contextKey string
+
+var contextKeyCredentials contextKey = "credentials"
+
+// LoadToken wraps the next handler, adding credentials to the request
+// context so subsequent handlers can access them efficiently via
+// CredentialsFromRequest.
+func LoadToken(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if _, ok := r.Context().Value(contextKeyCredentials).(*Credentials); !ok {
+ r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials, CredentialsFromRequest(r)))
+ }
+ next.ServeHTTP(w, r)
+ })
+}
+
+// RequireLiteralToken wraps the next handler, rejecting any request
+// that doesn't supply the given token. If the given token is empty,
+// RequireLiteralToken returns next (i.e., no auth checks are
+// performed).
+func RequireLiteralToken(token string, next http.Handler) http.Handler {
+ if token == "" {
+ return next
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c := CredentialsFromRequest(r)
+ if len(c.Tokens) == 0 {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
+ for _, t := range c.Tokens {
+ if t == token {
+ next.ServeHTTP(w, r)
+ return
+ }
+ }
+ http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ })
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+ check.TestingT(t)
+}
+
+var _ = check.Suite(&HandlersSuite{})
+
+type HandlersSuite struct {
+ served int
+ gotCredentials *Credentials
+}
+
+func (s *HandlersSuite) SetUpTest(c *check.C) {
+ s.served = 0
+ s.gotCredentials = nil
+}
+
+func (s *HandlersSuite) TestLoadToken(c *check.C) {
+ handler := LoadToken(s)
+ handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/foo/bar?api_token=xyzzy", nil))
+ c.Assert(s.gotCredentials, check.NotNil)
+ c.Assert(s.gotCredentials.Tokens, check.HasLen, 1)
+ c.Check(s.gotCredentials.Tokens[0], check.Equals, "xyzzy")
+}
+
+func (s *HandlersSuite) TestRequireLiteralTokenEmpty(c *check.C) {
+ handler := RequireLiteralToken("", s)
+
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=abcdef", nil))
+ c.Check(s.served, check.Equals, 1)
+ c.Check(w.Code, check.Equals, http.StatusOK)
+
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar", nil))
+ c.Check(s.served, check.Equals, 2)
+ c.Check(w.Code, check.Equals, http.StatusOK)
+}
+
+func (s *HandlersSuite) TestRequireLiteralToken(c *check.C) {
+ handler := RequireLiteralToken("xyzzy", s)
+
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=abcdef", nil))
+ c.Check(s.served, check.Equals, 0)
+ c.Check(w.Code, check.Equals, http.StatusForbidden)
+
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar", nil))
+ c.Check(s.served, check.Equals, 0)
+ c.Check(w.Code, check.Equals, http.StatusUnauthorized)
+
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=xyzzy", nil))
+ c.Check(s.served, check.Equals, 1)
+ c.Check(w.Code, check.Equals, http.StatusOK)
+ c.Assert(s.gotCredentials, check.NotNil)
+ c.Assert(s.gotCredentials.Tokens, check.HasLen, 1)
+ c.Check(s.gotCredentials.Tokens[0], check.Equals, "xyzzy")
+}
+
+func (s *HandlersSuite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ s.served++
+ s.gotCredentials = CredentialsFromRequest(r)
+}
}
func (agg *Aggregator) checkAuth(req *http.Request, cluster *arvados.Cluster) bool {
- creds := auth.NewCredentialsFromHTTPRequest(req)
+ creds := auth.CredentialsFromRequest(req)
for _, token := range creds.Tokens {
if token != "" && token == cluster.ManagementToken {
return true
defer srv.Close()
s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
Controller: arvados.SystemServiceInstance{Listen: listen},
+ Keepbalance: arvados.SystemServiceInstance{Listen: listen},
Keepproxy: arvados.SystemServiceInstance{Listen: listen},
Keepstore: arvados.SystemServiceInstance{Listen: listen},
Keepweb: arvados.SystemServiceInstance{Listen: listen},
defer srvU.Close()
s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
Controller: arvados.SystemServiceInstance{Listen: listenH},
+ Keepbalance: arvados.SystemServiceInstance{Listen: listenH},
Keepproxy: arvados.SystemServiceInstance{Listen: listenH},
Keepstore: arvados.SystemServiceInstance{Listen: listenH},
Keepweb: arvados.SystemServiceInstance{Listen: listenH},
"strings"
"time"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/stats"
"github.com/Sirupsen/logrus"
"github.com/gogo/protobuf/jsonpb"
// Returns an http.Handler that serves the Handler's metrics
// data at /metrics and /metrics.json, and passes other
// requests through to next.
- ServeAPI(next http.Handler) http.Handler
+ ServeAPI(token string, next http.Handler) http.Handler
}
type metrics struct {
// metrics API endpoints (currently "GET /metrics(.json)?") and passes
// other requests through to next.
//
+// If the given token is not empty, that token must be supplied by a
+// client in order to access the metrics endpoints.
+//
// Typical example:
//
// m := Instrument(...)
-// srv := http.Server{Handler: m.ServeAPI(m)}
-func (m *metrics) ServeAPI(next http.Handler) http.Handler {
+// srv := http.Server{Handler: m.ServeAPI("secrettoken", m)}
+func (m *metrics) ServeAPI(token string, next http.Handler) http.Handler {
+ jsonMetrics := auth.RequireLiteralToken(token, http.HandlerFunc(m.exportJSON))
+ plainMetrics := auth.RequireLiteralToken(token, m.exportProm)
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch {
case req.Method != "GET" && req.Method != "HEAD":
next.ServeHTTP(w, req)
case req.URL.Path == "/metrics.json":
- m.exportJSON(w, req)
+ jsonMetrics.ServeHTTP(w, req)
case req.URL.Path == "/metrics":
- m.exportProm.ServeHTTP(w, req)
+ plainMetrics.ServeHTTP(w, req)
default:
next.ServeHTTP(w, req)
}
@select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
end
end
-
- def load_filters_param
- super
- return if !params[:include_old_versions]
- @filters = @filters.map do |col, operator, operand|
- # Replace uuid filters when including past versions
- if col == 'uuid'
- ['current_version_uuid', operator, operand]
- else
- [col, operator, operand]
- end
- end
- end
end
if @object.locked_by_uuid != Thread.current[:api_client_authorization].uuid
raise ArvadosModel::PermissionDeniedError.new("Not locked by your token")
end
- @object = @object.auth
+ if @object.runtime_token.nil?
+ @object = @object.auth
+ else
+ @object = ApiClientAuthorization.validate(token: @object.runtime_token)
+ if @object.nil?
+ raise ArvadosModel::PermissionDeniedError.new("Invalid runtime_token")
+ end
+ end
show
end
if Thread.current[:api_client_authorization].nil?
send_error("Not logged in", status: 401)
else
- c = Container.where(auth_uuid: Thread.current[:api_client_authorization].uuid).first
- if c.nil?
+ @object = Container.for_current_token
+ if @object.nil?
send_error("Token is not associated with a container.", status: 404)
else
- @object = c
show
end
end
end
def secret_mounts
- if @object &&
- @object.auth_uuid &&
- @object.auth_uuid == Thread.current[:api_client_authorization].uuid
+ c = Container.for_current_token
+ if @object && c && @object.uuid == c.uuid
send_json({"secret_mounts" => @object.secret_mounts})
else
send_error("Token is not associated with this container.", status: 403)
case token[0..2]
when 'v2/'
- _, uuid, secret = token.split('/')
+ _, uuid, secret, optional = token.split('/')
unless uuid.andand.length == 27 && secret.andand.length.andand > 0
return nil
end
+ if !optional.nil?
+ # if "optional" is a container uuid, check that it
+ # matches expections.
+ c = Container.where(uuid: optional).first
+ if !c.nil?
+ if !c.auth_uuid.nil? and c.auth_uuid != uuid
+ # token doesn't match the container's token
+ return nil
+ end
+ if !c.runtime_token.nil? and "v2/#{uuid}/#{secret}" != c.runtime_token
+ # token doesn't match the container's token
+ return nil
+ end
+ if ![Container::Locked, Container::Running].include?(c.state)
+ # container isn't locked or running, token shouldn't be used
+ return nil
+ end
+ end
+ end
+
auth = ApiClientAuthorization.
includes(:user, :api_client).
where('uuid=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', uuid).
exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
end
- exclude_old_versions = ""
- if !include_old_versions && sql_table == "collections"
- exclude_old_versions = "AND #{sql_table}.uuid = #{sql_table}.current_version_uuid"
- end
-
if users_list.select { |u| u.is_admin }.any?
# Admin skips most permission checks, but still want to filter on trashed items.
if !include_trash
# Only include records where the owner is not trashed
sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
"WHERE trashed = 1 AND "+
- "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records} #{exclude_old_versions}"
+ "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
end
end
else
"(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
end
- sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records} #{exclude_old_versions}"
+ sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
+
+ end
+ if !include_old_versions && sql_table == "collections"
+ exclude_old_versions = "#{sql_table}.uuid = #{sql_table}.current_version_uuid"
+ if sql_conds.nil?
+ sql_conds = exclude_old_versions
+ else
+ sql_conds += " AND #{exclude_old_versions}"
+ end
end
self.where(sql_conds,
validate :ensure_pdh_matches_manifest_text
validate :ensure_storage_classes_desired_is_not_empty
validate :ensure_storage_classes_contain_non_empty_strings
- validate :current_versions_always_point_to_self, on: :update
+ validate :versioning_metadata_updates, on: :update
validate :past_versions_cannot_be_updated, on: :update
before_save :set_file_names
around_update :manage_versioning
# Restore requested changes on the current version
changes.keys.each do |attr|
- if attr == 'version'
- next
- elsif attr == 'preserve_version' && changes[attr].last == false
+ if attr == 'preserve_version' && changes[attr].last == false
next # Ignore false assignment, once true it'll be true until next version
end
self.attributes = {attr => changes[attr].last}
end
end
- def current_versions_always_point_to_self
+ def versioning_metadata_updates
+ valid = true
if (current_version_uuid_was == uuid_was) && current_version_uuid_changed?
errors.add(:current_version_uuid, "cannot be updated")
- false
+ valid = false
+ end
+ if version_changed?
+ errors.add(:version, "cannot be updated")
+ valid = false
end
+ valid
end
def assign_uuid
after_validation :assign_auth
before_save :sort_serialized_attrs
before_save :update_secret_mounts_md5
- before_save :scrub_secret_mounts
+ before_save :scrub_secrets
before_save :clear_runtime_status_when_queued
after_save :update_cr_logs
after_save :handle_completed
t.add :state
t.add :auth_uuid
t.add :scheduling_parameters
+ t.add :runtime_user_uuid
+ t.add :runtime_auth_scopes
end
# Supported states for a container
end
def self.full_text_searchable_columns
- super - ["secret_mounts", "secret_mounts_md5"]
+ super - ["secret_mounts", "secret_mounts_md5", "runtime_token"]
end
def self.searchable_columns *args
- super - ["secret_mounts_md5"]
+ super - ["secret_mounts_md5", "runtime_token"]
end
def logged_attributes
- super.except('secret_mounts')
+ super.except('secret_mounts', 'runtime_token')
end
def state_transitions
# Create a new container (or find an existing one) to satisfy the
# given container request.
def self.resolve(req)
- c_attrs = {
- command: req.command,
- cwd: req.cwd,
- environment: req.environment,
- output_path: req.output_path,
- container_image: resolve_container_image(req.container_image),
- mounts: resolve_mounts(req.mounts),
- runtime_constraints: resolve_runtime_constraints(req.runtime_constraints),
- scheduling_parameters: req.scheduling_parameters,
- secret_mounts: req.secret_mounts,
- }
+ if req.runtime_token.nil?
+ runtime_user = if req.modified_by_user_uuid.nil?
+ current_user
+ else
+ User.find_by_uuid(req.modified_by_user_uuid)
+ end
+ runtime_auth_scopes = ["all"]
+ else
+ auth = ApiClientAuthorization.validate(token: req.runtime_token)
+ if auth.nil?
+ raise ArgumentError.new "Invalid runtime token"
+ end
+ runtime_user = User.find_by_id(auth.user_id)
+ runtime_auth_scopes = auth.scopes
+ end
+ c_attrs = act_as_user runtime_user do
+ {
+ command: req.command,
+ cwd: req.cwd,
+ environment: req.environment,
+ output_path: req.output_path,
+ container_image: resolve_container_image(req.container_image),
+ mounts: resolve_mounts(req.mounts),
+ runtime_constraints: resolve_runtime_constraints(req.runtime_constraints),
+ scheduling_parameters: req.scheduling_parameters,
+ secret_mounts: req.secret_mounts,
+ runtime_token: req.runtime_token,
+ runtime_user_uuid: runtime_user.uuid,
+ runtime_auth_scopes: runtime_auth_scopes
+ }
+ end
act_as_system_user do
if req.use_existing && (reusable = find_reusable(c_attrs))
reusable
candidates = candidates.where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints]), md5: true)
log_reuse_info(candidates) { "after filtering on runtime_constraints #{attrs[:runtime_constraints].inspect}" }
+ candidates = candidates.where('runtime_user_uuid = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+ attrs[:runtime_user_uuid])
+ log_reuse_info(candidates) { "after filtering on runtime_user_uuid #{attrs[:runtime_user_uuid].inspect}" }
+
+ candidates = candidates.where('runtime_auth_scopes = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+ SafeJSON.dump(attrs[:runtime_auth_scopes].sort))
+ log_reuse_info(candidates) { "after filtering on runtime_auth_scopes #{attrs[:runtime_auth_scopes].inspect}" }
+
log_reuse_info { "checking for state=Complete with readable output and log..." }
select_readable_pdh = Collection.
[Complete, Cancelled].include?(self.state)
end
+ def self.for_current_token
+ return if !current_api_client_authorization
+ _, _, _, container_uuid = Thread.current[:token].split('/')
+ if container_uuid.nil?
+ Container.where(auth_uuid: current_api_client_authorization.uuid).first
+ else
+ Container.where('auth_uuid=? or (uuid=? and runtime_token=?)',
+ current_api_client_authorization.uuid,
+ container_uuid,
+ current_api_client_authorization.token).first
+ end
+ end
+
protected
def fill_field_defaults
permitted.push(:owner_uuid, :command, :container_image, :cwd,
:environment, :mounts, :output_path, :priority,
:runtime_constraints, :scheduling_parameters,
- :secret_mounts)
+ :secret_mounts, :runtime_token,
+ :runtime_user_uuid, :runtime_auth_scopes)
end
case self.state
def assign_auth
if self.auth_uuid_changed?
- return errors.add :auth_uuid, 'is readonly'
+ return errors.add :auth_uuid, 'is readonly'
end
if not [Locked, Running].include? self.state
# don't need one
# already have one
return
end
- cr = ContainerRequest.
- where('container_uuid=? and priority>0', self.uuid).
- order('priority desc').
- first
- if !cr
- return errors.add :auth_uuid, "cannot be assigned because priority <= 0"
+ if self.runtime_token.nil?
+ if self.runtime_user_uuid.nil?
+ # legacy behavior, we don't have a runtime_user_uuid so get
+ # the user from the highest priority container request, needed
+ # when performing an upgrade and there are queued containers,
+ # and some tests.
+ cr = ContainerRequest.
+ where('container_uuid=? and priority>0', self.uuid).
+ order('priority desc').
+ first
+ if !cr
+ return errors.add :auth_uuid, "cannot be assigned because priority <= 0"
+ end
+ self.runtime_user_uuid = cr.modified_by_user_uuid
+ self.runtime_auth_scopes = ["all"]
+ end
+
+ # generate a new token
+ self.auth = ApiClientAuthorization.
+ create!(user_id: User.find_by_uuid(self.runtime_user_uuid).id,
+ api_client_id: 0,
+ scopes: self.runtime_auth_scopes)
end
- self.auth = ApiClientAuthorization.
- create!(user_id: User.find_by_uuid(cr.modified_by_user_uuid).id,
- api_client_id: 0)
end
def sort_serialized_attrs
if self.scheduling_parameters_changed?
self.scheduling_parameters = self.class.deep_sort_hash(self.scheduling_parameters)
end
+ if self.runtime_auth_scopes_changed?
+ self.runtime_auth_scopes = self.runtime_auth_scopes.sort
+ end
end
def update_secret_mounts_md5
end
end
- def scrub_secret_mounts
+ def scrub_secrets
# this runs after update_secret_mounts_md5, so the
# secret_mounts_md5 will still reflect the secrets that are being
# scrubbed here.
if self.state_changed? && self.final?
self.secret_mounts = {}
+ self.runtime_token = nil
end
end
container_image: self.container_image,
mounts: self.mounts,
runtime_constraints: self.runtime_constraints,
- scheduling_parameters: self.scheduling_parameters
+ scheduling_parameters: self.scheduling_parameters,
+ secret_mounts: self.secret_mounts_was,
+ runtime_token: self.runtime_token_was,
+ runtime_user_uuid: self.runtime_user_uuid,
+ runtime_auth_scopes: self.runtime_auth_scopes
}
c = Container.create! c_attrs
retryable_requests.each do |cr|
validate :validate_state_change
validate :check_update_whitelist
validate :secret_mounts_key_conflict
- before_save :scrub_secret_mounts
+ validate :validate_runtime_token
+ before_save :scrub_secrets
before_create :set_requesting_container_uuid
before_destroy :set_priority_zero
after_save :update_priority
AttrsPermittedAlways = [:owner_uuid, :state, :name, :description, :properties]
AttrsPermittedBeforeCommit = [:command, :container_count_max,
:container_image, :cwd, :environment, :filters, :mounts,
- :output_path, :priority,
+ :output_path, :priority, :runtime_token,
:runtime_constraints, :state, :container_uuid, :use_existing,
:scheduling_parameters, :secret_mounts, :output_name, :output_ttl]
end
def logged_attributes
- super.except('secret_mounts')
+ super.except('secret_mounts', 'runtime_token')
end
def state_transitions
end
def skip_uuid_read_permission_check
- # XXX temporary until permissions are sorted out.
- %w(modified_by_client_uuid container_uuid requesting_container_uuid)
+ # The uuid_read_permission_check prevents users from making
+ # references to objects they can't view. However, in this case we
+ # don't want to do that check since there's a circular dependency
+ # where user can't view the container until the user has
+ # constructed the container request that references the container.
+ %w(container_uuid)
end
def finalize_if_needed
end
def self.full_text_searchable_columns
- super - ["mounts", "secret_mounts", "secret_mounts_md5"]
+ super - ["mounts", "secret_mounts", "secret_mounts_md5", "runtime_token"]
end
protected
end
end
- def scrub_secret_mounts
+ def validate_runtime_token
+ if !self.runtime_token.nil? && self.runtime_token_changed?
+ if !runtime_token[0..2] == "v2/"
+ errors.add :runtime_token, "not a v2 token"
+ return
+ end
+ if ApiClientAuthorization.validate(token: runtime_token).nil?
+ errors.add :runtime_token, "failed validation"
+ end
+ end
+ end
+
+ def scrub_secrets
if self.state == Final
self.secret_mounts = {}
+ self.runtime_token = nil
end
end
def get_requesting_container
return self.requesting_container_uuid if !self.requesting_container_uuid.nil?
- return if !current_api_client_authorization
- if (c = Container.where('auth_uuid=?', current_api_client_authorization.uuid).select([:uuid, :priority]).first)
- return c
- end
+ Container.for_current_token
end
end
--- /dev/null
+class AddContainerRuntimeToken < ActiveRecord::Migration
+ def change
+ add_column :container_requests, :runtime_token, :text, :null => true
+ add_column :containers, :runtime_user_uuid, :text, :null => true
+ add_column :containers, :runtime_auth_scopes, :jsonb, :null => true
+ end
+end
--- /dev/null
+class AddRuntimeTokenToContainer < ActiveRecord::Migration
+ def change
+ add_column :containers, :runtime_token, :text, :null => true
+ end
+end
log_uuid character varying(255),
output_name character varying(255) DEFAULT NULL::character varying,
output_ttl integer DEFAULT 0 NOT NULL,
- secret_mounts jsonb DEFAULT '{}'::jsonb
+ secret_mounts jsonb DEFAULT '{}'::jsonb,
+ runtime_token text
);
scheduling_parameters text,
secret_mounts jsonb DEFAULT '{}'::jsonb,
secret_mounts_md5 character varying DEFAULT '99914b932bd37a50b983c5e7c90ae93b'::character varying,
- runtime_status jsonb DEFAULT '{}'::jsonb
+ runtime_status jsonb DEFAULT '{}'::jsonb,
+ runtime_user_uuid text,
+ runtime_auth_scopes jsonb,
+ runtime_token text
);
INSERT INTO schema_migrations (version) VALUES ('20181001175023');
INSERT INTO schema_migrations (version) VALUES ('20181004131141');
+
+INSERT INTO schema_migrations (version) VALUES ('20181005192222');
+
+INSERT INTO schema_migrations (version) VALUES ('20181011184200');
+
where({group_class: 'project'}).
where('is_trashed = false and trash_at < statement_timestamp()').
update_all('is_trashed = true')
+
+ # Sweep expired tokens
+ ActiveRecord::Base.connection.execute("DELETE from api_client_authorizations where expires_at <= statement_timestamp()")
end
end
- GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w
- GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w/
- GET /arvados/v1/keep_services/accessible
+
+container_runtime_token:
+ uuid: zzzzz-gj3su-2nj68s291f50gd9
+ api_client: untrusted
+ user: container_runtime_token_user
+ api_token: 2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+ expires_at: 2038-01-01 00:00:00
+
+crt_user:
+ uuid: zzzzz-gj3su-3r47qqy5ja5d54v
+ api_client: untrusted
+ user: container_runtime_token_user
+ api_token: 13z1tz9deoryml3twep0vsahi4862097pe5lsmesugnkgpgpwk
+ expires_at: 2038-01-01 00:00:00
+
+runtime_token_limited_scope:
+ uuid: zzzzz-gj3su-2fljvypjrr4yr9m
+ api_client: untrusted
+ user: container_runtime_token_user
+ api_token: 1fwc3be1m13qkypix2gd01i4bq5ju483zjfc0cf4babjseirbm
+ expires_at: 2038-01-01 00:00:00
+ scopes: ["GET /"]
w_a_z_file_version_1:
uuid: zzzzz-4zz18-25k12570yk1ver1
current_version_uuid: zzzzz-4zz18-25k12570yk134b3
- portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
+ portable_data_hash: ba4ba4c7b99a58806b1ed70ea1263afe+45
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2015-02-09T10:53:38Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
modified_at: 2015-02-09T10:53:38Z
updated_at: 2015-02-09T10:53:38Z
- manifest_text: ". 4c6c2c0ac8aa0696edd7316a3be5ca3c+5 0:5:w\\040\\141\\040z\n"
+ manifest_text: ". 4d20280d5e516a0109768d49ab0f3318+3 0:3:waz\n"
name: "waz file"
version: 1
delete_at: 2038-01-01T00:00:00Z
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:expired\n"
name: expired_collection
+ version: 2
+
+expired_collection_past_version:
+ uuid: zzzzz-4zz18-mto52zx1s7oldie
+ current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3ih
+ portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-02-03T17:12:54Z
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+ modified_at: 2014-02-03T17:17:54Z
+ updated_at: 2014-02-03T17:17:54Z
+ is_trashed: true
+ trash_at: 2001-01-01T00:00:00Z
+ delete_at: 2038-01-01T00:00:00Z
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:expired\n"
+ name: expired_collection original
+ version: 1
trashed_on_next_sweep:
uuid: zzzzz-4zz18-4guozfh77ewd2f0
vcpus: 1
ram: 123
+runtime_token:
+ uuid: zzzzz-xvhdp-11eklkhy0n4dm86
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ name: queued
+ state: Committed
+ priority: 1
+ created_at: <%= 2.minute.ago.to_s(:db) %>
+ updated_at: <%= 1.minute.ago.to_s(:db) %>
+ modified_at: <%= 1.minute.ago.to_s(:db) %>
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ container_image: test
+ cwd: test
+ output_path: test
+ command: ["echo", "hello"]
+ container_uuid: zzzzz-dz642-20isqbkl8xwnsao
+ runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+ runtime_constraints:
+ vcpus: 1
+ ram: 123
+
# Test Helper trims the rest of the file
auth_uuid: zzzzz-gj3su-ty6lvu9d7u7c2sq
secret_mounts: {}
secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
+
+runtime_token:
+ uuid: zzzzz-dz642-20isqbkl8xwnsao
+ owner_uuid: zzzzz-tpzed-000000000000000
+ state: Locked
+ locked_by_uuid: zzzzz-gj3su-jrriu629zljsnuf
+ priority: 1
+ created_at: 2016-01-11 11:11:11.111111111 Z
+ updated_at: 2016-01-11 11:11:11.111111111 Z
+ container_image: test
+ cwd: test
+ output_path: test
+ command: ["echo", "hello"]
+ runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+ runtime_user_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+ runtime_auth_scopes: ["all"]
+ runtime_constraints:
+ ram: 12000000000
+ vcpus: 4
+ mounts:
+ /tmp:
+ kind: tmp
+ capacity: 24000000000
+ /var/spool/cwl:
+ kind: tmp
+ capacity: 24000000000
head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
properties: {}
+crt_user_permission_to_unlinked_docker_image_collection:
+ uuid: zzzzz-o0j2j-20zvdi9b4odcfz3
+ owner_uuid: zzzzz-tpzed-000000000000000
+ created_at: 2014-01-24 20:42:26 -0800
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-000000000000000
+ modified_at: 2014-01-24 20:42:26 -0800
+ updated_at: 2014-01-24 20:42:26 -0800
+ tail_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+ link_class: permission
+ name: can_read
+ head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
+ properties: {}
+
docker_image_collection_hash:
uuid: zzzzz-o0j2j-dockercollhasha
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
role: Computational biologist
getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+container_runtime_token_user:
+ owner_uuid: zzzzz-tpzed-000000000000000
+ uuid: zzzzz-tpzed-l3skomkti0c4vg4
+ email: spectator@arvados.local
+ first_name: Spect
+ last_name: Ator
+ identity_url: https://spectator.openid.local
+ is_active: true
+ is_admin: false
+ username: containerruntimetokenuser
+ prefs:
+ profile:
+ organization: example.com
+ role: Computational biologist
+ getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+
inactive_uninvited:
owner_uuid: zzzzz-tpzed-000000000000000
uuid: zzzzz-tpzed-rf2ec3ryh4vb5ma
assert_response 200
end
+ [:admin, :active].each do |user|
+ test "get trashed collection via filters and #{user} user" do
+ uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
+ authorize_with user
+ get :index, {
+ filters: [["current_version_uuid", "=", uuid]],
+ include_trash: true,
+ }
+ assert_response 200
+ # Only the current version is returned
+ assert_equal 1, json_response["items"].size
+ end
+ end
+
+ [:admin, :active].each do |user|
+ test "get trashed collection via filters and #{user} user, including its past versions" do
+ uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
+ authorize_with :admin
+ get :index, {
+ filters: [["current_version_uuid", "=", uuid]],
+ include_trash: true,
+ include_old_versions: true,
+ }
+ assert_response 200
+ # Both current & past version are returned
+ assert_equal 2, json_response["items"].size
+ end
+ end
+
+ test "trash collection also trash its past versions" do
+ uuid = collections(:collection_owned_by_active).uuid
+ authorize_with :active
+ versions = Collection.where(current_version_uuid: uuid)
+ assert_equal 2, versions.size
+ versions.each do |col|
+ refute col.is_trashed
+ end
+ post :trash, {
+ id: uuid,
+ }
+ assert_response 200
+ versions = Collection.where(current_version_uuid: uuid)
+ assert_equal 2, versions.size
+ versions.each do |col|
+ assert col.is_trashed
+ end
+ end
+
test 'get trashed collection without include_trash' do
uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
authorize_with :active
test 'can get collection with past versions' do
authorize_with :active
get :index, {
- filters: [['uuid','=',collections(:collection_owned_by_active).uuid]],
+ filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
include_old_versions: true
}
assert_response :success
req.reload
assert_equal 'bar', req.secret_mounts['/foo']['content']
end
+
+ test "runtime_token not in #create responses" do
+ authorize_with :active
+
+ post :create, {
+ container_request: minimal_cr.merge(
+ runtime_token: api_client_authorizations(:spectator).token)
+ }
+ assert_response :success
+
+ resp = JSON.parse(@response.body)
+ refute resp.has_key?('runtime_token')
+
+ req = ContainerRequest.where(uuid: resp['uuid']).first
+ assert_equal api_client_authorizations(:spectator).token, req.runtime_token
+ end
+
end
end
end
end
+
+ test 'get runtime_token auth' do
+ authorize_with :dispatch2
+ c = containers(:runtime_token)
+ get :auth, id: c.uuid
+ assert_response :success
+ assert_equal "v2/#{json_response['uuid']}/#{json_response['api_token']}", api_client_authorizations(:container_runtime_token).token
+ assert_equal 'arvados#apiClientAuthorization', json_response['kind']
+ end
+
end
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class ContainerAuthTest < ActionDispatch::IntegrationTest
+ fixtures :all
+
+ test "container token validate, Running, regular auth" do
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}/#{containers(:running).uuid}"}
+ # Container is Running, token can be used
+ assert_response :success
+ assert_equal containers(:running).uuid, json_response['uuid']
+ end
+
+ test "container token validate, Locked, runtime_token" do
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+ # Container is Running, token can be used
+ assert_response :success
+ assert_equal containers(:runtime_token).uuid, json_response['uuid']
+ end
+
+ test "container token validate, Cancelled, runtime_token" do
+ put "/arvados/v1/containers/#{containers(:runtime_token).uuid}", {
+ :format => :json,
+ :container => {:state => "Cancelled"}
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:dispatch1).token}"}
+ assert_response :success
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+ # Container is Queued, token cannot be used
+ assert_response 401
+ end
+
+ test "container token validate, Running, without optional portion" do
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}"}
+ # Container is Running, token can be used
+ assert_response :success
+ assert_equal containers(:running).uuid, json_response['uuid']
+ end
+
+ test "container token validate, Locked, runtime_token, without optional portion" do
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}"}
+ # runtime_token without container uuid won't return 'current'
+ assert_response 404
+ end
+
+ test "container token validate, wrong container uuid" do
+ get "/arvados/v1/containers/current", {
+ :format => :json
+ }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:running).uuid}"}
+ # Container uuid mismatch, token can't be used
+ assert_response 401
+ end
+end
assert_equal 'barney', json_response['username']
end
+ test "validate unsalted v2 token for remote cluster zbbbb" do
+ auth = api_client_authorizations(:active)
+ token = "v2/#{auth.uuid}/#{auth.api_token}"
+ get '/arvados/v1/users/current', {format: 'json', remote: 'zbbbb'}, {
+ "HTTP_AUTHORIZATION" => "Bearer #{token}"
+ }
+ assert_response :success
+ assert_equal(users(:active).uuid, json_response['uuid'])
+ end
+
+ test 'container request with runtime_token' do
+ [["valid local", "v2/#{api_client_authorizations(:active).uuid}/#{api_client_authorizations(:active).api_token}"],
+ ["valid remote", "v2/zbbbb-gj3su-000000000000000/abc"],
+ ["invalid local", "v2/#{api_client_authorizations(:active).uuid}/fakefakefake"],
+ ["invalid remote", "v2/zbork-gj3su-000000000000000/abc"],
+ ].each do |label, runtime_token|
+ post '/arvados/v1/container_requests', {
+ "container_request" => {
+ "command" => ["echo"],
+ "container_image" => "xyz",
+ "output_path" => "/",
+ "cwd" => "/",
+ "runtime_token" => runtime_token
+ }
+ }, {"HTTP_AUTHORIZATION" => "Bearer #{api_client_authorizations(:active).api_token}"}
+ if label.include? "invalid"
+ assert_response 422
+ else
+ assert_response :success
+ end
+ end
+ end
+
end
# SPDX-License-Identifier: AGPL-3.0
require 'test_helper'
+require 'sweep_trashed_objects'
class ApiClientAuthorizationTest < ActiveSupport::TestCase
include CurrentApiClient
assert_empty ApiClientAuthorization.where(api_token: newtoken), "Destroyed ApiClientAuth is still in database"
end
end
+
+ test "delete expired in SweepTrashedObjects" do
+ assert_not_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+ SweepTrashedObjects.sweep_now
+ assert_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+ end
+
end
end
end
+ [
+ ['version', 10],
+ ['current_version_uuid', 'zzzzz-4zz18-bv31uwvy3neko21'],
+ ].each do |name, new_value|
+ test "'#{name}' updates on current version collections are not allowed" do
+ act_as_user users(:active) do
+ # Set up initial collection
+ c = create_collection 'foo', Encoding::US_ASCII
+ assert c.valid?
+ assert_equal 1, c.version
+
+ assert_raises(ActiveRecord::RecordInvalid) do
+ c.update_attributes!({
+ name => new_value
+ })
+ end
+ end
+ end
+ end
+
test "uuid updates on current version make older versions update their pointers" do
Rails.configuration.collection_versioning = true
Rails.configuration.preserve_version_if_idle = 0
[
['running_container_auth', 'zzzzz-dz642-runningcontainr', 501],
- ['active_no_prefs', nil, 0],
+ ['active_no_prefs', nil, 0]
].each do |token, expected, expected_priority|
test "create as #{token} and expect requesting_container_uuid to be #{expected}" do
set_user_from_auth token
end
end
+ test "create as container_runtime_token and expect requesting_container_uuid to be zzzzz-dz642-20isqbkl8xwnsao" do
+ set_user_from_auth :container_runtime_token
+ Thread.current[:token] = "#{Thread.current[:token]}/zzzzz-dz642-20isqbkl8xwnsao"
+ cr = ContainerRequest.create(container_image: "img", output_path: "/tmp", command: ["echo", "foo"])
+ assert_not_nil cr.uuid, 'uuid should be set for newly created container_request'
+ assert_equal 'zzzzz-dz642-20isqbkl8xwnsao', cr.requesting_container_uuid
+ assert_equal 1, cr.priority
+ end
+
[[{"vcpus" => [2, nil]},
lambda { |resolved| resolved["vcpus"] == 2 }],
[{"vcpus" => [3, 7]},
assert_not_equal cr2.container_uuid, cr.container_uuid
end
+ test "Retry on container cancelled with runtime_token" do
+ set_user_from_auth :spectator
+ spec = api_client_authorizations(:active)
+ cr = create_minimal_req!(priority: 1, state: "Committed",
+ runtime_token: spec.token,
+ container_count_max: 2)
+ prev_container_uuid = cr.container_uuid
+
+ c = act_as_system_user do
+ c = Container.find_by_uuid(cr.container_uuid)
+ assert_equal spec.token, c.runtime_token
+ c.update_attributes!(state: Container::Locked)
+ c.update_attributes!(state: Container::Running)
+ c
+ end
+
+ cr.reload
+ assert_equal "Committed", cr.state
+ assert_equal prev_container_uuid, cr.container_uuid
+ prev_container_uuid = cr.container_uuid
+
+ act_as_system_user do
+ c.update_attributes!(state: Container::Cancelled)
+ end
+
+ cr.reload
+ assert_equal "Committed", cr.state
+ assert_not_equal prev_container_uuid, cr.container_uuid
+ prev_container_uuid = cr.container_uuid
+
+ c = act_as_system_user do
+ c = Container.find_by_uuid(cr.container_uuid)
+ assert_equal spec.token, c.runtime_token
+ c.update_attributes!(state: Container::Cancelled)
+ c
+ end
+
+ cr.reload
+ assert_equal "Final", cr.state
+ assert_equal prev_container_uuid, cr.container_uuid
+
+ end
+
test "Output collection name setting using output_name with name collision resolution" do
set_user_from_auth :active
output_name = 'unimaginative name'
secret_mounts: sm)
assert_equal [:secret_mounts], cr.errors.messages.keys
end
+
+ test "using runtime_token" do
+ set_user_from_auth :spectator
+ spec = api_client_authorizations(:active)
+ cr = create_minimal_req!(state: "Committed", runtime_token: spec.token, priority: 1)
+ cr.save!
+ c = Container.find_by_uuid cr.container_uuid
+ lock_and_run c
+ assert_nil c.auth_uuid
+ assert_equal c.runtime_token, spec.token
+
+ assert_not_nil ApiClientAuthorization.find_by_uuid(spec.uuid)
+
+ act_as_system_user do
+ c.update_attributes!(state: Container::Complete,
+ exit_code: 0,
+ output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
+ log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
+ end
+
+ cr.reload
+ c.reload
+ assert_nil cr.runtime_token
+ assert_nil c.runtime_token
+ end
+
+ test "invalid runtime_token" do
+ set_user_from_auth :active
+ spec = api_client_authorizations(:spectator)
+ assert_raises(ArgumentError) do
+ cr = create_minimal_req!(state: "Committed", runtime_token: "#{spec.token}xx")
+ cr.save!
+ end
+ end
end
"var" => "val",
},
secret_mounts: {},
+ runtime_user_uuid: "zzzzz-tpzed-xurymjxw79nv3jz",
+ runtime_auth_scopes: ["all"]
}
+ def request_only attrs
+ attrs.reject {|k| [:runtime_user_uuid, :runtime_auth_scopes].include? k}
+ end
+
def minimal_new attrs={}
- cr = ContainerRequest.new DEFAULT_ATTRS.merge(attrs)
+ cr = ContainerRequest.new request_only(DEFAULT_ATTRS.merge(attrs))
cr.state = ContainerRequest::Committed
- act_as_user users(:active) do
- cr.save!
- end
+ cr.save!
c = Container.find_by_uuid cr.container_uuid
assert_not_nil c
return c, cr
end
test "Container serialized hash attributes sorted before save" do
+ set_user_from_auth :active
env = {"C" => "3", "B" => "2", "A" => "1"}
m = {"F" => {"kind" => "3"}, "E" => {"kind" => "2"}, "D" => {"kind" => "1"}}
rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1}
end
test "find_reusable method should select higher priority queued container" do
+ Rails.configuration.log_reuse_decisions = true
set_user_from_auth :active
common_attrs = REUSABLE_COMMON_ATTRS.merge({environment:{"var" => "queued"}})
c_low_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:1}))
log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
}
- cr = ContainerRequest.new common_attrs
+ cr = ContainerRequest.new request_only(common_attrs)
cr.use_existing = false
cr.state = ContainerRequest::Committed
cr.save!
c_output1 = Container.where(uuid: cr.container_uuid).first
- cr = ContainerRequest.new common_attrs
+ cr = ContainerRequest.new request_only(common_attrs)
cr.use_existing = false
cr.state = ContainerRequest::Committed
cr.save!
c_output2.update_attributes!({state: Container::Running})
c_output2.update_attributes!(completed_attrs.merge({log: log1, output: out2}))
- reused = Container.resolve(ContainerRequest.new(common_attrs))
+ set_user_from_auth :active
+ reused = Container.resolve(ContainerRequest.new(request_only(common_attrs)))
assert_equal c_output1.uuid, reused.uuid
end
Container.find_reusable(REUSABLE_COMMON_ATTRS)
end
+ def runtime_token_attr tok
+ auth = api_client_authorizations(tok)
+ {runtime_user_uuid: User.find_by_id(auth.user_id).uuid,
+ runtime_auth_scopes: auth.scopes,
+ runtime_token: auth.token}
+ end
+
+ test "find_reusable method with same runtime_token" do
+ set_user_from_auth :active
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:container_runtime_token).token}))
+ assert_equal Container::Queued, c1.state
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_not_nil reused
+ assert_equal reused.uuid, c1.uuid
+ end
+
+ test "find_reusable method with different runtime_token, same user" do
+ set_user_from_auth :active
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:crt_user).token}))
+ assert_equal Container::Queued, c1.state
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_not_nil reused
+ assert_equal reused.uuid, c1.uuid
+ end
+
+ test "find_reusable method with nil runtime_token, then runtime_token with same user" do
+ set_user_from_auth :crt_user
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs)
+ assert_equal Container::Queued, c1.state
+ assert_equal users(:container_runtime_token_user).uuid, c1.runtime_user_uuid
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_not_nil reused
+ assert_equal reused.uuid, c1.uuid
+ end
+
+ test "find_reusable method with different runtime_token, different user" do
+ set_user_from_auth :crt_user
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:active).token}))
+ assert_equal Container::Queued, c1.state
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_nil reused
+ end
+
+ test "find_reusable method with nil runtime_token, then runtime_token with different user" do
+ set_user_from_auth :active
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs.merge({runtime_token: nil}))
+ assert_equal Container::Queued, c1.state
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_nil reused
+ end
+
+ test "find_reusable method with different runtime_token, different scope, same user" do
+ set_user_from_auth :active
+ common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+ c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:runtime_token_limited_scope).token}))
+ assert_equal Container::Queued, c1.state
+ reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+ assert_nil reused
+ end
+
test "Container running" do
+ set_user_from_auth :active
c, _ = minimal_new priority: 1
set_user_from_auth :dispatch1
end
test "Lock and unlock" do
+ set_user_from_auth :active
c, cr = minimal_new priority: 0
set_user_from_auth :dispatch1
end
test "Container queued cancel" do
+ set_user_from_auth :active
c, cr = minimal_new({container_count_max: 1})
set_user_from_auth :dispatch1
assert c.update_attributes(state: Container::Cancelled), show_errors(c)
end
test "Container locked cancel" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
assert c.lock, show_errors(c)
end
test "Container locked cancel with log" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
assert c.lock, show_errors(c)
end
test "Container running cancel" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "Container only set exit code on complete" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "locked_by_uuid can update log when locked/running, and output when running" do
+ set_user_from_auth :active
logcoll = collections(:real_log_collection)
c, cr1 = minimal_new
cr2 = ContainerRequest.new(DEFAULT_ATTRS)
end
test "auth_uuid can set output, progress, runtime_status, state on running container -- but not log" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "not allowed to set output that is not readable by current user" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "other token cannot set output on running container" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "can set trashed output on running container" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
end
test "not allowed to set trashed output that is not readable by current user" do
+ set_user_from_auth :active
c, _ = minimal_new
set_user_from_auth :dispatch1
c.lock
{state: Container::Complete, exit_code: 0, output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'},
{state: Container::Cancelled},
].each do |final_attrs|
- test "secret_mounts is null after container is #{final_attrs[:state]}" do
+ test "secret_mounts and runtime_token are null after container is #{final_attrs[:state]}" do
+ set_user_from_auth :active
c, cr = minimal_new(secret_mounts: {'/secret' => {'kind' => 'text', 'content' => 'foo'}},
- container_count_max: 1)
+ container_count_max: 1, runtime_token: api_client_authorizations(:active).token)
set_user_from_auth :dispatch1
c.lock
c.update_attributes!(state: Container::Running)
c.reload
assert c.secret_mounts.has_key?('/secret')
+ assert_equal api_client_authorizations(:active).token, c.runtime_token
c.update_attributes!(final_attrs)
c.reload
assert_equal({}, c.secret_mounts)
+ assert_nil c.runtime_token
cr.reload
assert_equal({}, cr.secret_mounts)
+ assert_nil cr.runtime_token
assert_no_secrets_logged
end
end
httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
}()
- creds := auth.NewCredentialsFromHTTPRequest(r)
+ creds := auth.CredentialsFromRequest(r)
if len(creds.Tokens) == 0 {
statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
ContainerStart(ctx context.Context, container string, options dockertypes.ContainerStartOptions) error
ContainerRemove(ctx context.Context, container string, options dockertypes.ContainerRemoveOptions) error
ContainerWait(ctx context.Context, container string, condition dockercontainer.WaitCondition) (<-chan dockercontainer.ContainerWaitOKBody, <-chan error)
+ ContainerInspect(ctx context.Context, id string) (dockertypes.ContainerJSON, error)
ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error)
ImageLoad(ctx context.Context, input io.Reader, quiet bool) (dockertypes.ImageLoadResponse, error)
ImageRemove(ctx context.Context, image string, options dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error)
cStateLock sync.Mutex
cCancelled bool // StopContainer() invoked
+ cRemoved bool // docker confirmed the container no longer exists
enableNetwork string // one of "default" or "always"
networkMode string // passed through to HostConfig.NetworkMode
arvMountLog *ThrottledLogger
checkContainerd time.Duration
+
+ containerWatchdogInterval time.Duration
}
// setupSignals sets up signal handling to gracefully terminate the underlying
if err != nil {
runner.CrunchLog.Printf("error removing container: %s", err)
}
+ if err == nil || strings.Contains(err.Error(), "No such container: "+runner.ContainerID) {
+ runner.cRemoved = true
+ }
}
var errorBlacklist = []string{
runTimeExceeded = time.After(time.Duration(timeout) * time.Second)
}
+ containerGone := make(chan struct{})
+ go func() {
+ defer close(containerGone)
+ if runner.containerWatchdogInterval < 1 {
+ runner.containerWatchdogInterval = time.Minute
+ }
+ for range time.NewTicker(runner.containerWatchdogInterval).C {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(runner.containerWatchdogInterval))
+ ctr, err := runner.Docker.ContainerInspect(ctx, runner.ContainerID)
+ cancel()
+ runner.cStateLock.Lock()
+ done := runner.cRemoved || runner.ExitCode != nil
+ runner.cStateLock.Unlock()
+ if done {
+ return
+ } else if err != nil {
+ runner.CrunchLog.Printf("Error inspecting container: %s", err)
+ runner.checkBrokenNode(err)
+ return
+ } else if ctr.State == nil || !(ctr.State.Running || ctr.State.Status == "created") {
+ runner.CrunchLog.Printf("Container is not running: State=%v", ctr.State)
+ return
+ }
+ }
+ }()
+
containerdGone := make(chan error)
defer close(containerdGone)
if runner.checkContainerd > 0 {
runner.stop(nil)
runTimeExceeded = nil
+ case <-containerGone:
+ return errors.New("docker client never returned status")
+
case err := <-containerdGone:
return err
}
if err != nil {
return "", err
}
- runner.token = auth.APIToken
+ runner.token = fmt.Sprintf("v2/%s/%s/%s", auth.UUID, auth.APIToken, runner.Container.UUID)
return runner.token, nil
}
type TestSuite struct {
client *arvados.Client
docker *TestDockerClient
+ runner *ContainerRunner
}
func (s *TestSuite) SetUpTest(c *C) {
api *ArvTestClient
realTemp string
calledWait bool
+ ctrExited bool
}
func NewTestDockerClient() *TestDockerClient {
return body, err
}
+func (t *TestDockerClient) ContainerInspect(ctx context.Context, id string) (c dockertypes.ContainerJSON, err error) {
+ c.ContainerJSONBase = &dockertypes.ContainerJSONBase{}
+ c.ID = "abcde"
+ if t.ctrExited {
+ c.State = &dockertypes.ContainerState{Status: "exited", Dead: true}
+ } else {
+ c.State = &dockertypes.ContainerState{Status: "running", Pid: 1234, Running: true}
+ }
+ return
+}
+
func (t *TestDockerClient) ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error) {
if t.exitCode == 2 {
return dockertypes.ImageInspect{}, nil, fmt.Errorf("Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?")
defer kc.Close()
cr, err = NewContainerRunner(s.client, api, kc, s.docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
c.Assert(err, IsNil)
+ s.runner = cr
cr.statInterval = 100 * time.Millisecond
+ cr.containerWatchdogInterval = time.Second
am := &ArvMountCmdLine{}
cr.RunArvMount = am.ArvMountTest
c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*maximum run time exceeded.*")
}
+func (s *TestSuite) TestContainerWaitFails(c *C) {
+ api, _, _ := s.fullRunHelper(c, `{
+ "command": ["sleep", "3"],
+ "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
+ "cwd": ".",
+ "mounts": {"/tmp": {"kind": "tmp"} },
+ "output_path": "/tmp",
+ "priority": 1
+}`, nil, 0, func(t *TestDockerClient) {
+ t.ctrExited = true
+ time.Sleep(10 * time.Second)
+ t.logWriter.Close()
+ })
+
+ c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
+ c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*Container is not running.*")
+}
+
func (s *TestSuite) TestCrunchstat(c *C) {
api, _, _ := s.fullRunHelper(c, `{
"command": ["sleep", "1"],
Description=Arvados Docker Image Cleaner
Documentation=https://doc.arvados.org/
After=network.target
-AssertPathExists=/etc/arvados/docker-cleaner/docker-cleaner.json
+#AssertPathExists=/etc/arvados/docker-cleaner/docker-cleaner.json
# systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
StartLimitInterval=0
"fmt"
"log"
"math"
- "os"
"runtime"
"sort"
"strings"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
+ "github.com/Sirupsen/logrus"
)
-// CheckConfig returns an error if anything is wrong with the given
-// config and runOptions.
-func CheckConfig(config Config, runOptions RunOptions) error {
- if len(config.KeepServiceList.Items) > 0 && config.KeepServiceTypes != nil {
- return fmt.Errorf("cannot specify both KeepServiceList and KeepServiceTypes in config")
- }
- if !runOptions.Once && config.RunPeriod == arvados.Duration(0) {
- return fmt.Errorf("you must either use the -once flag, or specify RunPeriod in config")
- }
- return nil
-}
-
// Balancer compares the contents of keepstore servers with the
// collections stored in Arvados, and issues pull/trash requests
// needed to get (closer to) the optimal data layout.
// BlobSignatureTTL; and all N existing replicas of a given data block
// are in the N best positions in rendezvous probe order.
type Balancer struct {
+ Logger *logrus.Logger
+ Dumper *logrus.Logger
+ Metrics *metrics
+
*BlockStateMap
KeepServices map[string]*KeepService
DefaultReplication int
- Logger *log.Logger
- Dumper *log.Logger
MinMtime int64
classes []string
func (bal *Balancer) Run(config Config, runOptions RunOptions) (nextRunOptions RunOptions, err error) {
nextRunOptions = runOptions
- bal.Dumper = runOptions.Dumper
- bal.Logger = runOptions.Logger
- if bal.Logger == nil {
- bal.Logger = log.New(os.Stderr, "", log.LstdFlags)
- }
-
- defer timeMe(bal.Logger, "Run")()
+ defer bal.time("sweep", "wall clock time to run one full sweep")()
if len(config.KeepServiceList.Items) > 0 {
err = bal.SetKeepServices(config.KeepServiceList)
//
// It encodes the resulting information in BlockStateMap.
func (bal *Balancer) GetCurrentState(c *arvados.Client, pageSize, bufs int) error {
- defer timeMe(bal.Logger, "GetCurrentState")()
+ defer bal.time("get_state", "wall clock time to get current state")()
bal.BlockStateMap = NewBlockStateMap()
dd, err := c.DiscoveryDocument()
bal.DefaultReplication = dd.DefaultCollectionReplication
bal.MinMtime = time.Now().UnixNano() - dd.BlobSignatureTTL*1e9
- errs := make(chan error, 2+len(bal.KeepServices))
+ errs := make(chan error, 1)
wg := sync.WaitGroup{}
// When a device is mounted more than once, we will get its
bal.logf("mount %s: retrieve index from %s", mounts[0], mounts[0].KeepService)
idx, err := mounts[0].KeepService.IndexMount(c, mounts[0].UUID, "")
if err != nil {
- errs <- fmt.Errorf("%s: retrieve index: %v", mounts[0], err)
+ select {
+ case errs <- fmt.Errorf("%s: retrieve index: %v", mounts[0], err):
+ default:
+ }
return
}
if len(errs) > 0 {
return
}
for _, mount := range mounts {
- bal.logf("%s: add %d replicas to map", mount, len(idx))
+ bal.logf("%s: add %d entries to map", mount, len(idx))
bal.BlockStateMap.AddReplicas(mount, idx)
- bal.logf("%s: added %d replicas", mount, len(idx))
+ bal.logf("%s: added %d entries to map at %dx (%d replicas)", mount, len(idx), mount.Replication, len(idx)*mount.Replication)
}
bal.logf("mount %s: index done", mounts[0])
}(mounts)
for coll := range collQ {
err := bal.addCollection(coll)
if err != nil {
- errs <- err
+ select {
+ case errs <- err:
+ default:
+ }
for range collQ {
}
return
})
close(collQ)
if err != nil {
- errs <- err
+ select {
+ case errs <- err:
+ default:
+ }
}
}()
func (bal *Balancer) ComputeChangeSets() {
// This just calls balanceBlock() once for each block, using a
// pool of worker goroutines.
- defer timeMe(bal.Logger, "ComputeChangeSets")()
+ defer bal.time("changeset_compute", "wall clock time to compute changesets")()
bal.setupLookupTables()
type balanceTask struct {
slots = append(slots, slot{
mnt: mnt,
repl: repl,
- want: repl != nil && (mnt.ReadOnly || repl.Mtime >= bal.MinMtime),
+ want: repl != nil && mnt.ReadOnly,
})
}
}
// Prefer a mount that satisfies the
// desired class.
return bal.mountsByClass[class][si.mnt]
- } else if wanti, wantj := si.want, si.want; wanti != wantj {
+ } else if si.want != sj.want {
// Prefer a mount that will have a
// replica no matter what we do here
// -- either because it already has an
// untrashable replica, or because we
// already need it to satisfy a
// different storage class.
- return slots[i].want
+ return si.want
} else if orderi, orderj := srvRendezvous[si.mnt.KeepService], srvRendezvous[sj.mnt.KeepService]; orderi != orderj {
// Prefer a better rendezvous
// position.
// TODO: request a Touch if Mtime is duplicated.
var change int
switch {
- case !underreplicated && slot.repl != nil && !slot.want && !unsafeToDelete[slot.repl.Mtime]:
+ case !underreplicated && !slot.want && slot.repl != nil && slot.repl.Mtime < bal.MinMtime && !unsafeToDelete[slot.repl.Mtime]:
slot.mnt.KeepService.AddTrash(Trash{
SizedDigest: blkid,
Mtime: slot.repl.Mtime,
trashes int
replHistogram []int
classStats map[string]replicationStats
+
+ // collectionBytes / collectionBlockBytes = deduplication ratio
+ collectionBytes int64 // sum(bytes in referenced blocks) across all collections
+ collectionBlockBytes int64 // sum(block size) across all blocks referenced by collections
+ collectionBlockRefs int64 // sum(number of blocks referenced) across all collections
+ collectionBlocks int64 // number of blocks referenced by any collection
+}
+
+func (s *balancerStats) dedupByteRatio() float64 {
+ if s.collectionBlockBytes == 0 {
+ return 0
+ }
+ return float64(s.collectionBytes) / float64(s.collectionBlockBytes)
+}
+
+func (s *balancerStats) dedupBlockRatio() float64 {
+ if s.collectionBlocks == 0 {
+ return 0
+ }
+ return float64(s.collectionBlockRefs) / float64(s.collectionBlocks)
}
type replicationStats struct {
surplus := result.have - result.want
bytes := result.blkid.Size()
+ if rc := int64(result.blk.RefCount); rc > 0 {
+ s.collectionBytes += rc * bytes
+ s.collectionBlockBytes += bytes
+ s.collectionBlockRefs += rc
+ s.collectionBlocks++
+ }
+
for class, state := range result.classState {
cs := s.classStats[class]
if state.unachievable {
s.trashes += len(srv.ChangeSet.Trashes)
}
bal.stats = s
+ bal.Metrics.UpdateStats(s)
}
// PrintStatistics writes statistics about the computed changes to
// existing blocks that are either underreplicated or poorly
// distributed according to rendezvous hashing.
func (bal *Balancer) CommitPulls(c *arvados.Client) error {
+ defer bal.time("send_pull_lists", "wall clock time to send pull lists")()
return bal.commitAsync(c, "send pull list",
func(srv *KeepService) error {
return srv.CommitPulls(c)
// keepstore servers. This has the effect of deleting blocks that are
// overreplicated or unreferenced.
func (bal *Balancer) CommitTrash(c *arvados.Client) error {
+ defer bal.time("send_trash_lists", "wall clock time to send trash lists")()
return bal.commitAsync(c, "send trash list",
func(srv *KeepService) error {
return srv.CommitTrash(c)
var err error
defer func() { errs <- err }()
label := fmt.Sprintf("%s: %v", srv, label)
- defer timeMe(bal.Logger, label)()
err = f(srv)
if err != nil {
err = fmt.Errorf("%s: %v", label, err)
}
}
+func (bal *Balancer) time(name, help string) func() {
+ observer := bal.Metrics.DurationObserver(name+"_seconds", help)
+ t0 := time.Now()
+ bal.Logger.Printf("%s: start", name)
+ return func() {
+ dur := time.Since(t0)
+ observer.Observe(dur.Seconds())
+ bal.Logger.Printf("%s: took %vs", name, dur.Seconds())
+ }
+}
+
// Rendezvous hash sort function. Less efficient than sorting on
// precomputed rendezvous hashes, but also rarely used.
func rendezvousLess(i, j string, blkid arvados.SizedDigest) bool {
"fmt"
"io"
"io/ioutil"
- "log"
"net/http"
"net/http/httptest"
"strings"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
-
+ "github.com/Sirupsen/logrus"
check "gopkg.in/check.v1"
)
if strings.Contains(r.Form.Get("filters"), `modified_at`) {
io.WriteString(w, `{"items_available":0,"items":[]}`)
} else {
- io.WriteString(w, `{"items_available":2,"items":[
+ io.WriteString(w, `{"items_available":3,"items":[
+ {"uuid":"zzzzz-4zz18-aaaaaaaaaaaaaaa","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
{"uuid":"zzzzz-4zz18-ehbhgtheo8909or","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
{"uuid":"zzzzz-4zz18-znfnqtbbv4spc3w","portable_data_hash":"1f4b0bc7583c2a7f9102c395f4ffc5e3+45","manifest_text":". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo\n","modified_at":"2014-02-03T17:22:54Z"}]}`)
}
}
// make a log.Logger that writes to the current test's c.Log().
-func (s *runSuite) logger(c *check.C) *log.Logger {
+func (s *runSuite) logger(c *check.C) *logrus.Logger {
r, w := io.Pipe()
go func() {
buf := make([]byte, 10000)
}
}
}()
- return log.New(w, "", log.LstdFlags)
+ logger := logrus.New()
+ logger.Out = w
+ return logger
}
func (s *runSuite) SetUpTest(c *check.C) {
AuthToken: "xyzzy",
APIHost: "zzzzz.arvadosapi.com",
Client: s.stub.Start()},
- KeepServiceTypes: []string{"disk"}}
+ KeepServiceTypes: []string{"disk"},
+ RunPeriod: arvados.Duration(time.Second),
+ }
s.stub.serveDiscoveryDoc()
s.stub.logf = c.Logf
}
s.stub.serveKeepstoreIndexFoo4Bar1()
trashReqs := s.stub.serveKeepstoreTrash()
pullReqs := s.stub.serveKeepstorePull()
- _, err := (&Balancer{}).Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ _, err = srv.Run()
c.Check(err, check.ErrorMatches, "received zero collections")
c.Check(trashReqs.Count(), check.Equals, 4)
c.Check(pullReqs.Count(), check.Equals, 0)
s.stub.serveKeepstoreMounts()
indexReqs := s.stub.serveKeepstoreIndexFoo4Bar1()
trashReqs := s.stub.serveKeepstoreTrash()
- _, err := (&Balancer{}).Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ _, err = srv.Run()
c.Check(err, check.IsNil)
c.Check(indexReqs.Count(), check.Equals, 0)
c.Check(trashReqs.Count(), check.Equals, 0)
s.stub.serveKeepstoreMounts()
trashReqs := s.stub.serveKeepstoreTrash()
pullReqs := s.stub.serveKeepstorePull()
- _, err := (&Balancer{}).Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ _, err = srv.Run()
c.Check(err, check.ErrorMatches, "current user .* is not .* admin user")
c.Check(trashReqs.Count(), check.Equals, 0)
c.Check(pullReqs.Count(), check.Equals, 0)
s.stub.serveKeepstoreIndexFoo4Bar1()
trashReqs := s.stub.serveKeepstoreTrash()
pullReqs := s.stub.serveKeepstorePull()
- _, err := (&Balancer{}).Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ _, err = srv.Run()
c.Check(err, check.ErrorMatches, `Retrieved 2 collections with modtime <= .* but server now reports there are 3 collections.*`)
c.Check(trashReqs.Count(), check.Equals, 4)
c.Check(pullReqs.Count(), check.Equals, 0)
s.stub.serveKeepstoreIndexFoo4Bar1()
trashReqs := s.stub.serveKeepstoreTrash()
pullReqs := s.stub.serveKeepstorePull()
- var bal Balancer
- _, err := bal.Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ bal, err := srv.Run()
c.Check(err, check.IsNil)
for _, req := range collReqs.reqs {
c.Check(req.Form.Get("include_trash"), check.Equals, "true")
+ c.Check(req.Form.Get("include_old_versions"), check.Equals, "true")
}
c.Check(trashReqs.Count(), check.Equals, 0)
c.Check(pullReqs.Count(), check.Equals, 0)
}
func (s *runSuite) TestCommit(c *check.C) {
+ s.config.Listen = ":"
+ s.config.ManagementToken = "xyzzy"
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
s.stub.serveKeepstoreIndexFoo4Bar1()
trashReqs := s.stub.serveKeepstoreTrash()
pullReqs := s.stub.serveKeepstorePull()
- var bal Balancer
- _, err := bal.Run(s.config, opts)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+ bal, err := srv.Run()
c.Check(err, check.IsNil)
c.Check(trashReqs.Count(), check.Equals, 8)
c.Check(pullReqs.Count(), check.Equals, 4)
// "bar" block is underreplicated by 1, and its only copy is
// in a poor rendezvous position
c.Check(bal.stats.pulls, check.Equals, 2)
+
+ metrics := s.getMetrics(c, srv)
+ c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_total_bytes 15\n.*`)
+ c.Check(metrics, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_sum [0-9\.]+\n.*`)
+ c.Check(metrics, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count 1\n.*`)
+ c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_dedup_byte_ratio 1\.5\n.*`)
+ c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_dedup_block_ratio 1\.5\n.*`)
}
func (s *runSuite) TestRunForever(c *check.C) {
+ s.config.Listen = ":"
+ s.config.ManagementToken = "xyzzy"
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
stop := make(chan interface{})
s.config.RunPeriod = arvados.Duration(time.Millisecond)
- go RunForever(s.config, opts, stop)
+ srv, err := NewServer(s.config, opts)
+ c.Assert(err, check.IsNil)
+
+ done := make(chan bool)
+ go func() {
+ srv.RunForever(stop)
+ close(done)
+ }()
// Each run should send 4 pull lists + 4 trash lists. The
// first run should also send 4 empty trash lists at
time.Sleep(time.Millisecond)
}
stop <- true
+ <-done
c.Check(pullReqs.Count() >= 16, check.Equals, true)
c.Check(trashReqs.Count(), check.Equals, pullReqs.Count()+4)
+ c.Check(s.getMetrics(c, srv), check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count `+fmt.Sprintf("%d", pullReqs.Count()/4)+`\n.*`)
+}
+
+func (s *runSuite) getMetrics(c *check.C, srv *Server) string {
+ resp, err := http.Get("http://" + srv.listening + "/metrics")
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+ resp, err = http.Get("http://" + srv.listening + "/metrics?api_token=xyzzy")
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+ buf, err := ioutil.ReadAll(resp.Body)
+ c.Check(err, check.IsNil)
+ return string(buf)
}
bal.try(c, tester{
desired: map[string]int{"default": 2},
current: slots{0, 1, 2},
- timestamps: []int64{oldTime, newTime, newTime + 1}})
+ timestamps: []int64{oldTime, newTime, newTime + 1},
+ expectResult: balanceResult{
+ have: 3,
+ want: 2,
+ classState: map[string]balancedBlockState{"default": {
+ desired: 2,
+ surplus: 1,
+ unachievable: false}}}})
// The best replicas are too new to delete, but the excess
// replica is old enough.
bal.try(c, tester{
// replicas actually stored (according to the keepstore indexes we
// know about).
type BlockState struct {
+ RefCount int
Replicas []Replica
Desired map[string]int
// TODO: Support combinations of classes ("private + durable")
}
func (bs *BlockState) increaseDesired(classes []string, n int) {
+ bs.RefCount++
if len(classes) == 0 {
classes = defaultClasses
}
}
expectCount, err := countCollections(c, arvados.ResourceListParams{
- IncludeTrash: true,
+ IncludeTrash: true,
+ IncludeOldVersions: true,
})
if err != nil {
return err
limit = 1<<31 - 1
}
params := arvados.ResourceListParams{
- Limit: &limit,
- Order: "modified_at, uuid",
- Count: "none",
- Select: []string{"uuid", "unsigned_manifest_text", "modified_at", "portable_data_hash", "replication_desired"},
- IncludeTrash: true,
+ Limit: &limit,
+ Order: "modified_at, uuid",
+ Count: "none",
+ Select: []string{"uuid", "unsigned_manifest_text", "modified_at", "portable_data_hash", "replication_desired"},
+ IncludeTrash: true,
+ IncludeOldVersions: true,
}
var last arvados.Collection
var filterTime time.Time
Attr: "modified_at",
Operator: "<=",
Operand: filterTime}},
- IncludeTrash: true,
+ IncludeTrash: true,
+ IncludeOldVersions: true,
}); err != nil {
return err
} else if callCount < checkCount {
import (
"bytes"
- "log"
"os"
"strings"
"testing"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
-
+ "github.com/Sirupsen/logrus"
check "gopkg.in/check.v1"
)
Insecure: true,
},
KeepServiceTypes: []string{"disk"},
+ RunPeriod: arvados.Duration(time.Second),
}
}
var logBuf *bytes.Buffer
for iter := 0; iter < 20; iter++ {
logBuf := &bytes.Buffer{}
+ logger := logrus.New()
+ logger.Out = logBuf
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
- Logger: log.New(logBuf, "", log.LstdFlags),
+ Logger: logger,
+ }
+
+ bal := &Balancer{
+ Logger: logger,
+ Metrics: newMetrics(),
}
- nextOpts, err := (&Balancer{}).Run(s.config, opts)
+ nextOpts, err := bal.Run(s.config, opts)
c.Check(err, check.IsNil)
c.Check(nextOpts.SafeRendezvousState, check.Not(check.Equals), "")
c.Check(nextOpts.CommitPulls, check.Equals, true)
ExecStart=/usr/bin/keep-balance -commit-pulls -commit-trash
Restart=always
RestartSec=10s
+Nice=19
# systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
StartLimitInterval=0
"log"
"net/http"
"os"
- "os/signal"
- "syscall"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/config"
+ "github.com/Sirupsen/logrus"
)
-var version = "dev"
-
-const defaultConfigPath = "/etc/arvados/keep-balance/keep-balance.yml"
-
-// Config specifies site configuration, like API credentials and the
-// choice of which servers are to be balanced.
-//
-// Config is loaded from a JSON config file (see usage()).
-type Config struct {
- // Arvados API endpoint and credentials.
- Client arvados.Client
-
- // List of service types (e.g., "disk") to balance.
- KeepServiceTypes []string
-
- KeepServiceList arvados.KeepServiceList
-
- // How often to check
- RunPeriod arvados.Duration
-
- // Number of collections to request in each API call
- CollectionBatchSize int
-
- // Max collections to buffer in memory (bigger values consume
- // more memory, but can reduce store-and-forward latency when
- // fetching pages)
- CollectionBuffers int
-
- // Timeout for outgoing http request/response cycle.
- RequestTimeout arvados.Duration
-}
-
-// RunOptions controls runtime behavior. The flags/options that belong
-// here are the ones that are useful for interactive use. For example,
-// "CommitTrash" is a runtime option rather than a config item because
-// it invokes a troubleshooting feature rather than expressing how
-// balancing is meant to be done at a given site.
-//
-// RunOptions fields are controlled by command line flags.
-type RunOptions struct {
- Once bool
- CommitPulls bool
- CommitTrash bool
- Logger *log.Logger
- Dumper *log.Logger
-
- // SafeRendezvousState from the most recent balance operation,
- // or "" if unknown. If this changes from one run to the next,
- // we need to watch out for races. See
- // (*Balancer)ClearTrashLists.
- SafeRendezvousState string
-}
-
var debugf = func(string, ...interface{}) {}
func main() {
}
}
if *dumpFlag {
- runOptions.Dumper = log.New(os.Stdout, "", log.LstdFlags)
+ runOptions.Dumper = logrus.New()
+ runOptions.Dumper.Out = os.Stdout
+ runOptions.Dumper.Formatter = &logrus.TextFormatter{}
}
- err := CheckConfig(cfg, runOptions)
+ srv, err := NewServer(cfg, runOptions)
if err != nil {
// (don't run)
} else if runOptions.Once {
- _, err = (&Balancer{}).Run(cfg, runOptions)
+ _, err = srv.Run()
} else {
- err = RunForever(cfg, runOptions, nil)
+ err = srv.RunForever(nil)
}
if err != nil {
log.Fatal(err)
log.Fatal(err)
}
}
-
-// RunForever runs forever, or (for testing purposes) until the given
-// stop channel is ready to receive.
-func RunForever(config Config, runOptions RunOptions, stop <-chan interface{}) error {
- if runOptions.Logger == nil {
- runOptions.Logger = log.New(os.Stderr, "", log.LstdFlags)
- }
- logger := runOptions.Logger
-
- ticker := time.NewTicker(time.Duration(config.RunPeriod))
-
- // The unbuffered channel here means we only hear SIGUSR1 if
- // it arrives while we're waiting in select{}.
- sigUSR1 := make(chan os.Signal)
- signal.Notify(sigUSR1, syscall.SIGUSR1)
-
- logger.Printf("starting up: will scan every %v and on SIGUSR1", config.RunPeriod)
-
- for {
- if !runOptions.CommitPulls && !runOptions.CommitTrash {
- logger.Print("WARNING: Will scan periodically, but no changes will be committed.")
- logger.Print("======= Consider using -commit-pulls and -commit-trash flags.")
- }
-
- bal := &Balancer{}
- var err error
- runOptions, err = bal.Run(config, runOptions)
- if err != nil {
- logger.Print("run failed: ", err)
- } else {
- logger.Print("run succeeded")
- }
-
- select {
- case <-stop:
- signal.Stop(sigUSR1)
- return nil
- case <-ticker.C:
- logger.Print("timer went off")
- case <-sigUSR1:
- logger.Print("received SIGUSR1, resetting timer")
- // Reset the timer so we don't start the N+1st
- // run too soon after the Nth run is triggered
- // by SIGUSR1.
- ticker.Stop()
- ticker = time.NewTicker(time.Duration(config.RunPeriod))
- }
- logger.Print("starting next run")
- }
-}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sync"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+type observer interface{ Observe(float64) }
+type setter interface{ Set(float64) }
+
+type metrics struct {
+ reg *prometheus.Registry
+ statsGauges map[string]setter
+ observers map[string]observer
+ setupOnce sync.Once
+ mtx sync.Mutex
+}
+
+func newMetrics() *metrics {
+ return &metrics{
+ reg: prometheus.NewRegistry(),
+ statsGauges: map[string]setter{},
+ observers: map[string]observer{},
+ }
+}
+
+func (m *metrics) DurationObserver(name, help string) observer {
+ m.mtx.Lock()
+ defer m.mtx.Unlock()
+ if obs, ok := m.observers[name]; ok {
+ return obs
+ }
+ summary := prometheus.NewSummary(prometheus.SummaryOpts{
+ Namespace: "arvados",
+ Name: name,
+ Subsystem: "keepbalance",
+ Help: help,
+ })
+ m.reg.MustRegister(summary)
+ m.observers[name] = summary
+ return summary
+}
+
+// UpdateStats updates prometheus metrics using the given
+// balancerStats. It creates and registers the needed gauges on its
+// first invocation.
+func (m *metrics) UpdateStats(s balancerStats) {
+ type gauge struct {
+ Value interface{}
+ Help string
+ }
+ s2g := map[string]gauge{
+ "total": {s.current, "current backend storage usage"},
+ "garbage": {s.garbage, "garbage (unreferenced, old)"},
+ "transient": {s.unref, "transient (unreferenced, new)"},
+ "overreplicated": {s.overrep, "overreplicated"},
+ "underreplicated": {s.underrep, "underreplicated"},
+ "lost": {s.lost, "lost"},
+ "dedup_byte_ratio": {s.dedupByteRatio(), "deduplication ratio, bytes referenced / bytes stored"},
+ "dedup_block_ratio": {s.dedupBlockRatio(), "deduplication ratio, blocks referenced / blocks stored"},
+ }
+ m.setupOnce.Do(func() {
+ // Register gauge(s) for each balancerStats field.
+ addGauge := func(name, help string) {
+ g := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: "arvados",
+ Name: name,
+ Subsystem: "keep",
+ Help: help,
+ })
+ m.reg.MustRegister(g)
+ m.statsGauges[name] = g
+ }
+ for name, gauge := range s2g {
+ switch gauge.Value.(type) {
+ case blocksNBytes:
+ for _, sub := range []string{"blocks", "bytes", "replicas"} {
+ addGauge(name+"_"+sub, sub+" of "+gauge.Help)
+ }
+ case int, int64, float64:
+ addGauge(name, gauge.Help)
+ default:
+ panic(fmt.Sprintf("bad gauge type %T", gauge.Value))
+ }
+ }
+ })
+ // Set gauges to values from s.
+ for name, gauge := range s2g {
+ switch val := gauge.Value.(type) {
+ case blocksNBytes:
+ m.statsGauges[name+"_blocks"].Set(float64(val.blocks))
+ m.statsGauges[name+"_bytes"].Set(float64(val.bytes))
+ m.statsGauges[name+"_replicas"].Set(float64(val.replicas))
+ case int:
+ m.statsGauges[name].Set(float64(val))
+ case int64:
+ m.statsGauges[name].Set(float64(val))
+ case float64:
+ m.statsGauges[name].Set(float64(val))
+ default:
+ panic(fmt.Sprintf("bad gauge type %T", gauge.Value))
+ }
+ }
+}
+
+func (m *metrics) Handler(log promhttp.Logger) http.Handler {
+ return promhttp.HandlerFor(m.reg, promhttp.HandlerOpts{
+ ErrorLog: log,
+ })
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
+ "git.curoverse.com/arvados.git/sdk/go/httpserver"
+ "github.com/Sirupsen/logrus"
+)
+
+var version = "dev"
+
+const (
+ defaultConfigPath = "/etc/arvados/keep-balance/keep-balance.yml"
+ rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+)
+
+// Config specifies site configuration, like API credentials and the
+// choice of which servers are to be balanced.
+//
+// Config is loaded from a JSON config file (see usage()).
+type Config struct {
+ // Arvados API endpoint and credentials.
+ Client arvados.Client
+
+ // List of service types (e.g., "disk") to balance.
+ KeepServiceTypes []string
+
+ KeepServiceList arvados.KeepServiceList
+
+ // address, address:port, or :port for management interface
+ Listen string
+
+ // token for management APIs
+ ManagementToken string
+
+ // How often to check
+ RunPeriod arvados.Duration
+
+ // Number of collections to request in each API call
+ CollectionBatchSize int
+
+ // Max collections to buffer in memory (bigger values consume
+ // more memory, but can reduce store-and-forward latency when
+ // fetching pages)
+ CollectionBuffers int
+
+ // Timeout for outgoing http request/response cycle.
+ RequestTimeout arvados.Duration
+}
+
+// RunOptions controls runtime behavior. The flags/options that belong
+// here are the ones that are useful for interactive use. For example,
+// "CommitTrash" is a runtime option rather than a config item because
+// it invokes a troubleshooting feature rather than expressing how
+// balancing is meant to be done at a given site.
+//
+// RunOptions fields are controlled by command line flags.
+type RunOptions struct {
+ Once bool
+ CommitPulls bool
+ CommitTrash bool
+ Logger *logrus.Logger
+ Dumper *logrus.Logger
+
+ // SafeRendezvousState from the most recent balance operation,
+ // or "" if unknown. If this changes from one run to the next,
+ // we need to watch out for races. See
+ // (*Balancer)ClearTrashLists.
+ SafeRendezvousState string
+}
+
+type Server struct {
+ config Config
+ runOptions RunOptions
+ metrics *metrics
+ listening string // for tests
+
+ Logger *logrus.Logger
+ Dumper *logrus.Logger
+}
+
+// NewServer returns a new Server that runs Balancers using the given
+// config and runOptions.
+func NewServer(config Config, runOptions RunOptions) (*Server, error) {
+ if len(config.KeepServiceList.Items) > 0 && config.KeepServiceTypes != nil {
+ return nil, fmt.Errorf("cannot specify both KeepServiceList and KeepServiceTypes in config")
+ }
+ if !runOptions.Once && config.RunPeriod == arvados.Duration(0) {
+ return nil, fmt.Errorf("you must either use the -once flag, or specify RunPeriod in config")
+ }
+
+ if runOptions.Logger == nil {
+ log := logrus.New()
+ log.Formatter = &logrus.JSONFormatter{
+ TimestampFormat: rfc3339NanoFixed,
+ }
+ log.Out = os.Stderr
+ runOptions.Logger = log
+ }
+
+ srv := &Server{
+ config: config,
+ runOptions: runOptions,
+ metrics: newMetrics(),
+ Logger: runOptions.Logger,
+ Dumper: runOptions.Dumper,
+ }
+ return srv, srv.start()
+}
+
+func (srv *Server) start() error {
+ if srv.config.Listen == "" {
+ return nil
+ }
+ server := &httpserver.Server{
+ Server: http.Server{
+ Handler: httpserver.LogRequests(srv.Logger,
+ auth.RequireLiteralToken(srv.config.ManagementToken,
+ srv.metrics.Handler(srv.Logger))),
+ },
+ Addr: srv.config.Listen,
+ }
+ err := server.Start()
+ if err != nil {
+ return err
+ }
+ srv.Logger.Printf("listening at %s", server.Addr)
+ srv.listening = server.Addr
+ return nil
+}
+
+func (srv *Server) Run() (*Balancer, error) {
+ bal := &Balancer{
+ Logger: srv.Logger,
+ Dumper: srv.Dumper,
+ Metrics: srv.metrics,
+ }
+ var err error
+ srv.runOptions, err = bal.Run(srv.config, srv.runOptions)
+ return bal, err
+}
+
+// RunForever runs forever, or (for testing purposes) until the given
+// stop channel is ready to receive.
+func (srv *Server) RunForever(stop <-chan interface{}) error {
+ logger := srv.runOptions.Logger
+
+ ticker := time.NewTicker(time.Duration(srv.config.RunPeriod))
+
+ // The unbuffered channel here means we only hear SIGUSR1 if
+ // it arrives while we're waiting in select{}.
+ sigUSR1 := make(chan os.Signal)
+ signal.Notify(sigUSR1, syscall.SIGUSR1)
+
+ logger.Printf("starting up: will scan every %v and on SIGUSR1", srv.config.RunPeriod)
+
+ for {
+ if !srv.runOptions.CommitPulls && !srv.runOptions.CommitTrash {
+ logger.Print("WARNING: Will scan periodically, but no changes will be committed.")
+ logger.Print("======= Consider using -commit-pulls and -commit-trash flags.")
+ }
+
+ _, err := srv.Run()
+ if err != nil {
+ logger.Print("run failed: ", err)
+ } else {
+ logger.Print("run succeeded")
+ }
+
+ select {
+ case <-stop:
+ signal.Stop(sigUSR1)
+ return nil
+ case <-ticker.C:
+ logger.Print("timer went off")
+ case <-sigUSR1:
+ logger.Print("received SIGUSR1, resetting timer")
+ // Reset the timer so we don't start the N+1st
+ // run too soon after the Nth run is triggered
+ // by SIGUSR1.
+ ticker.Stop()
+ ticker = time.NewTicker(time.Duration(srv.config.RunPeriod))
+ }
+ logger.Print("starting next run")
+ }
+}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-package main
-
-import (
- "log"
- "time"
-)
-
-func timeMe(logger *log.Logger, label string) func() {
- t0 := time.Now()
- logger.Printf("%s: start", label)
- return func() {
- logger.Printf("%s: took %v", label, time.Since(t0))
- }
-}
Insecure: false
KeepServiceTypes:
- disk
+Listen: ":9005"
+ManagementToken: xyzzy
RunPeriod: 600s
CollectionBatchSize: 100000
CollectionBuffers: 1000
if useSiteFS {
if tokens == nil {
- tokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ tokens = auth.CredentialsFromRequest(r).Tokens
}
h.serveSiteFS(w, r, tokens, credentialsOK, attachment)
return
if tokens == nil {
if credentialsOK {
- reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ reqTokens = auth.CredentialsFromRequest(r).Tokens
}
tokens = append(reqTokens, h.Config.AnonymousTokens...)
}
c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
}
+func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
+ s.testServer.Config.AttachmentOnlyHost = "download.example.com"
+ resp := s.testVhostRedirectTokenToCookie(c, "GET",
+ "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
+ "?api_token="+arvadostest.ActiveToken,
+ "",
+ "",
+ http.StatusOK,
+ "waz",
+ )
+ c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
+ resp = s.testVhostRedirectTokenToCookie(c, "GET",
+ "download.example.com/by_id/"+arvadostest.WazVersion1Collection+"/waz",
+ "?api_token="+arvadostest.ActiveToken,
+ "",
+ "",
+ http.StatusOK,
+ "waz",
+ )
+ c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
+}
+
func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
s.testServer.Config.TrustAllContent = true
s.testVhostRedirectTokenToCookie(c, "GET",
header: authHeader,
expect: nil,
},
+ {
+ uri: "download.example.com/c=" + arvadostest.WazVersion1Collection,
+ header: authHeader,
+ expect: []string{"waz"},
+ cutDirs: 1,
+ },
+ {
+ uri: "download.example.com/by_id/" + arvadostest.WazVersion1Collection,
+ header: authHeader,
+ expect: []string{"waz"},
+ cutDirs: 2,
+ },
} {
c.Logf("HTML: %q => %q", trial.uri, trial.expect)
resp := httptest.NewRecorder()
reg := prometheus.NewRegistry()
h.Config.Cache.registry = reg
mh := httpserver.Instrument(reg, nil, httpserver.AddRequestIDs(httpserver.LogRequests(nil, h)))
- h.MetricsAPI = mh.ServeAPI(http.NotFoundHandler())
+ h.MetricsAPI = mh.ServeAPI(h.Config.ManagementToken, http.NotFoundHandler())
srv.Handler = mh
srv.Addr = srv.Config.Listen
return srv.Server.Start()
req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
resp, err = http.DefaultClient.Do(req)
c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+ req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+ req.Header.Set("Authorization", "Bearer badtoken")
+ resp, err = http.DefaultClient.Do(req)
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusForbidden)
+
+ req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+ req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+ resp, err = http.DefaultClient.Do(req)
+ c.Assert(err, check.IsNil)
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
type summary struct {
SampleCount string `json:"sample_count"`
kc.PutB([]byte("Hello world\n"))
kc.PutB([]byte("foo"))
kc.PutB([]byte("foobar"))
+ kc.PutB([]byte("waz"))
}
func (s *IntegrationSuite) TearDownSuite(c *check.C) {
Insecure: true,
}
cfg.Listen = "127.0.0.1:0"
+ cfg.ManagementToken = arvadostest.ManagementToken
s.testServer = &server{Config: cfg}
err := s.testServer.Start()
c.Assert(err, check.Equals, nil)
systemAuthToken string
debugLogf func(string, ...interface{})
- ManagementToken string `doc: The secret key that must be provided by monitoring services
-wishing to access the health check endpoint (/_health).`
+ ManagementToken string
}
var (
rtr.limiter = httpserver.NewRequestLimiter(theConfig.MaxRequests, rtr)
- stack := httpserver.Instrument(nil, nil,
+ instrumented := httpserver.Instrument(nil, nil,
httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter)))
- return stack.ServeAPI(stack)
+ return instrumented.ServeAPI(theConfig.ManagementToken, instrumented)
}
// BadRequestHandler is a HandleFunc to address bad requests.
KeepVM = s.vm
theConfig = DefaultConfig()
theConfig.systemAuthToken = arvadostest.DataManagerToken
+ theConfig.ManagementToken = arvadostest.ManagementToken
theConfig.Start()
s.rtr = MakeRESTRouter(testCluster)
}
s.call("PUT", "/"+TestHash, "", TestBlock)
s.call("PUT", "/"+TestHash2, "", TestBlock2)
resp := s.call("GET", "/metrics.json", "", nil)
+ c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+ resp = s.call("GET", "/metrics.json", "foobar", nil)
+ c.Check(resp.Code, check.Equals, http.StatusForbidden)
+ resp = s.call("GET", "/metrics.json", arvadostest.ManagementToken, nil)
c.Check(resp.Code, check.Equals, http.StatusOK)
var j []struct {
Name string
resp := httptest.NewRecorder()
req, _ := http.NewRequest(method, path, bytes.NewReader(body))
if tok != "" {
- req.Header.Set("Authorization", "OAuth2 "+tok)
+ req.Header.Set("Authorization", "Bearer "+tok)
}
s.rtr.ServeHTTP(resp, req)
return resp
include agpl-3.0.txt
include README.rst
-include arvados_version.py
\ No newline at end of file
+include arvados_version.py
+include arvados-node-manager.service
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+[Unit]
+Description=Arvados Node Manager Daemon
+Documentation=https://doc.arvados.org/
+After=network.target
+AssertPathExists=/etc/arvados-node-manager/config.ini
+
+# systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
+StartLimitInterval=0
+
+# systemd>=230 (debian:9) obeys StartLimitIntervalSec in the [Unit] section
+StartLimitIntervalSec=0
+
+[Service]
+EnvironmentFile=-/etc/default/arvados-node-manager
+LimitDATA=3145728K
+LimitRSS=3145728K
+LimitMEMLOCK=3145728K
+LimitNOFILE=10240
+Type=simple
+ExecStart=/usr/bin/env sh -c '/usr/bin/arvados-node-manager --foreground --config /etc/arvados-node-manager/config.ini 2>&1 | cat'
+Restart=always
+RestartSec=1
+
+# systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
packages=find_packages(),
scripts=['bin/arvados-node-manager'],
data_files=[
- ('share/doc/arvados-node-manager', ['agpl-3.0.txt', 'README.rst']),
+ ('share/doc/arvados-node-manager', ['agpl-3.0.txt', 'README.rst', 'arvados-node-manager.service']),
],
install_requires=[
'apache-libcloud>=2.3.1.dev1',
curl -L -f ${PJSURL} | tar -C /usr/local -xjf - && \
ln -s ../phantomjs-${PJSVERSION}-linux-x86_64/bin/phantomjs /usr/local/bin
+ENV GDVERSION=v0.23.0
+ENV GDURL=https://github.com/mozilla/geckodriver/releases/download/$GDVERSION/geckodriver-$GDVERSION-linux64.tar.gz
+RUN set -e && curl -L -f ${GDURL} | tar -C /usr/local/bin -xzf - geckodriver
+
RUN pip install -U setuptools
ENV NODEVERSION v6.11.4