From a390fe1153f11db2d86831d8072bc518a54f9c32 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 20 Nov 2020 09:26:17 +0200 Subject: [PATCH 001/516] RCC-111: minor unrelated fixes (v6.0.7) - added liveonly flag to prevent ROBOCORP_HOME/base copy (for containers) - added colorless flag - minor fix to Mac vs Linux exit codes in unit tests --- cmd/root.go | 2 ++ common/variables.go | 1 + common/version.go | 2 +- conda/workflows.go | 8 ++++++-- pretty/variables.go | 5 +++-- shell/transparent_test.go | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 299153de..bc25aa75 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,7 +81,9 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP/rcc.yaml)") rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") + rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") rootCmd.PersistentFlags().BoolVarP(&pathlib.Lockless, "lockless", "", false, "do not use file locking ... DANGER!") + rootCmd.PersistentFlags().BoolVarP(&pretty.Colorless, "colorless", "", false, "do not use colors in CLI UI") rootCmd.PersistentFlags().BoolVarP(&common.NoCache, "nocache", "", false, "do not use cache for credentials and tokens, always request them from cloud") rootCmd.PersistentFlags().BoolVarP(&common.DebugFlag, "debug", "", false, "to get debug output where available (not for production use)") diff --git a/common/variables.go b/common/variables.go index 5ade2fb8..440959ee 100644 --- a/common/variables.go +++ b/common/variables.go @@ -5,6 +5,7 @@ var ( DebugFlag bool TraceFlag bool NoCache bool + Liveonly bool ) const ( diff --git a/common/version.go b/common/version.go index bb051ef3..979e5560 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.0.6` + Version = `v6.0.7` ) diff --git a/conda/workflows.go b/conda/workflows.go index 57e7d836..8f6e67e7 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -215,8 +215,12 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { if newLive(condaYaml, requirementsText, key, force, freshInstall) { misses += 1 xviper.Set("stats.env.miss", misses) - common.Log("#### Progress: 3/4 [backup new environment as template]") - CloneFromTo(liveFolder, TemplateFrom(key)) + if !common.Liveonly { + common.Log("#### Progress: 3/4 [backup new environment as template]") + CloneFromTo(liveFolder, TemplateFrom(key)) + } else { + common.Log("#### Progress: 3/4 [skipped]") + } return liveFolder, nil } diff --git a/pretty/variables.go b/pretty/variables.go index 6f661bfe..4db330b2 100644 --- a/pretty/variables.go +++ b/pretty/variables.go @@ -8,6 +8,7 @@ import ( ) var ( + Colorless bool Iconic bool Disabled bool Interactive bool @@ -32,7 +33,7 @@ func Setup() { localSetup() common.Trace("Interactive mode enabled: %v; colors enabled: %v; icons enabled: %v", Interactive, !Disabled, Iconic) - if Interactive && !Disabled { + if Interactive && !Disabled && !Colorless { White = csi("97m") Grey = csi("90m") Black = csi("30m") @@ -42,7 +43,7 @@ func Setup() { Yellow = csi("93m") Reset = csi("0m") } - if Iconic { + if Iconic && !Colorless { Sparkles = "\u2728 " Rocket = "\U0001F680 " } diff --git a/shell/transparent_test.go b/shell/transparent_test.go index 4f04a6af..92466771 100644 --- a/shell/transparent_test.go +++ b/shell/transparent_test.go @@ -25,5 +25,5 @@ func TestCanExecuteSimpleEcho(t *testing.T) { code, err = shell.New(nil, ".", "ls", "-l", "crapiti.crap").Transparent() wont_be.Nil(err) - must_be.Equal(2, code) + wont_be.Equal(0, code) } From 735b8264e8086113eca542310f766bb7c21ca436 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 23 Nov 2020 13:37:40 +0200 Subject: [PATCH 002/516] RCC-110: interactive rcc create (v6.0.8) - now defaulting to previous alternate create command - old interactive promptui create is removed - go modules updated --- cmd/wizardcreate.go | 16 ++---- common/version.go | 2 +- go.mod | 6 +-- go.sum | 48 +++++------------- wizard/altcreate.go | 117 -------------------------------------------- wizard/create.go | 83 +++++++++++++++++++++++-------- 6 files changed, 80 insertions(+), 192 deletions(-) delete mode 100644 wizard/altcreate.go diff --git a/cmd/wizardcreate.go b/cmd/wizardcreate.go index 9b9e5012..ca9c648b 100644 --- a/cmd/wizardcreate.go +++ b/cmd/wizardcreate.go @@ -8,8 +8,6 @@ import ( "github.com/spf13/cobra" ) -var altFlag bool - var wizardCreateCmd = &cobra.Command{ Use: "create", Short: "Create a directory structure for a robot interactively.", @@ -21,16 +19,9 @@ var wizardCreateCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Interactive create lasted").Report() } - if altFlag { - err := wizard.AltCreate(args) - if err != nil { - pretty.Exit(2, "%v", err) - } - } else { - err := wizard.Create(args) - if err != nil { - pretty.Exit(2, "%v", err) - } + err := wizard.Create(args) + if err != nil { + pretty.Exit(2, "%v", err) } }, } @@ -38,5 +29,4 @@ var wizardCreateCmd = &cobra.Command{ func init() { interactiveCmd.AddCommand(wizardCreateCmd) rootCmd.AddCommand(wizardCreateCmd) - wizardCreateCmd.Flags().BoolVarP(&altFlag, "alt", "a", false, "select alternative create command") } diff --git a/common/version.go b/common/version.go index 979e5560..4f337d7f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.0.7` + Version = `v6.0.8` ) diff --git a/go.mod b/go.mod index 9c9ef3a4..9cb681fd 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,11 @@ module github.com/robocorp/rcc go 1.14 require ( - github.com/fsnotify/fsnotify v1.4.9 + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284 github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/gorilla/mux v1.7.4 - github.com/manifoldco/promptui v0.8.0 github.com/mattn/go-isatty v0.0.12 - github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/pelletier/go-toml v1.6.0 // indirect github.com/spf13/afero v1.2.2 // indirect @@ -21,7 +18,6 @@ require ( github.com/spf13/viper v1.6.2 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect golang.org/x/text v0.3.2 // indirect - golang.org/x/tools v0.0.0-20200331202046-9d5940d49312 // indirect gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 25df6784..870da76f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -7,10 +8,6 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -18,6 +15,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -26,10 +24,9 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/glaslos/tlsh v0.2.0 h1:9zr1gNyYCAMMsirzU5FFlUEEWp5hsrFE+B4LZEg8psk= -github.com/glaslos/tlsh v0.2.0/go.mod h1:S/OBGINihiGogV6WoaLeMY2UrS5Rl1iqMnplLonIOI4= github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284 h1:AWxbfoKclxMucKNN1csTKj1OqypRsbjnBtVpfMUzeuQ= github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284/go.mod h1:Fg7YBN7EUtifZmdJrQOQHvebtw5RF89IX7nWFsmaqeE= +github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M= github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -47,9 +44,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -59,28 +55,22 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= -github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -97,6 +87,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -111,7 +102,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -138,6 +131,7 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -145,40 +139,28 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= @@ -193,19 +175,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200331202046-9d5940d49312 h1:2PHG+Ia3gK1K2kjxZnSylizb//eyaMG8gDFbOG7wLV8= -golang.org/x/tools v0.0.0-20200331202046-9d5940d49312/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/wizard/altcreate.go b/wizard/altcreate.go deleted file mode 100644 index 627ad954..00000000 --- a/wizard/altcreate.go +++ /dev/null @@ -1,117 +0,0 @@ -package wizard - -import ( - "bufio" - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/pretty" -) - -const ( - newline = '\n' -) - -var ( - namePattern = regexp.MustCompile("^[\\w-]*$") - digitPattern = regexp.MustCompile("^\\d+$") -) - -func ask(question, defaults string, validator *regexp.Regexp, erratic string) (string, error) { - for { - common.Stdout("%s? %s%s %s[%s]:%s ", pretty.Green, pretty.White, question, pretty.Grey, defaults, pretty.Reset) - source := bufio.NewReader(os.Stdin) - reply, err := source.ReadString(newline) - common.Stdout("\n") - if err != nil { - return "", err - } - reply = strings.TrimSpace(reply) - if !validator.MatchString(reply) { - common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) - continue - } - if len(reply) == 0 { - return defaults, nil - } - return reply, nil - } -} - -func choose(question, label string, candidates []string) (string, error) { - keys := []string{} - common.Stdout("%s%s:%s\n", pretty.Grey, label, pretty.Reset) - for index, candidate := range candidates { - key := index + 1 - keys = append(keys, fmt.Sprintf("%d", key)) - common.Stdout(" %s%2d: %s%s%s\n", pretty.Grey, key, pretty.White, candidate, pretty.Reset) - } - common.Stdout("\n") - selectable := strings.Join(keys, "|") - pattern, err := regexp.Compile(fmt.Sprintf("^(?:%s)?$", selectable)) - if err != nil { - return "", err - } - reply, err := ask(question, "1", pattern, "Give selections number from above list.") - if err != nil { - return "", err - } - selected, err := strconv.Atoi(reply) - if err != nil { - return "", err - } - return candidates[selected-1], nil -} - -func AltCreate(arguments []string) error { - common.Stdout("\n") - - warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - robotName, err := ask("Give robot name", firstOf(arguments, "my-first-robot"), namePattern, "Use just normal english characters and no spaces!") - - if err != nil { - return err - } - - fullpath, err := filepath.Abs(robotName) - if err != nil { - return err - } - - if pathlib.IsDir(fullpath) { - return errors.New(fmt.Sprintf("Folder %s already exists. Try with other name.", robotName)) - } - - selected, err := choose("Choose a template", "Templates", operations.ListTemplates()) - if err != nil { - return err - } - - common.Stdout("%sCreating the %s%s%s robot: %s%s%s\n", pretty.White, pretty.Cyan, selected, pretty.White, pretty.Cyan, robotName, pretty.Reset) - common.Stdout("\n") - - err = operations.InitializeWorkarea(fullpath, selected, false) - if err != nil { - return err - } - - common.Stdout("%s%s%sThe %s robot has been created to: %s%s\n", pretty.Yellow, pretty.Sparkles, pretty.Green, selected, robotName, pretty.Reset) - common.Stdout("\n") - - common.Stdout("%s%sGet started with following commands:%s\n", pretty.White, pretty.Rocket, pretty.Reset) - common.Stdout("\n") - - common.Stdout("%s$ %scd %s%s\n", pretty.Grey, pretty.Cyan, robotName, pretty.Reset) - common.Stdout("%s$ %srcc run%s\n", pretty.Grey, pretty.Cyan, pretty.Reset) - common.Stdout("\n") - - return nil -} diff --git a/wizard/create.go b/wizard/create.go index e37165eb..ed3f1c80 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -1,28 +1,81 @@ package wizard import ( + "bufio" "errors" "fmt" + "os" "path/filepath" + "regexp" + "strconv" + "strings" - "github.com/manifoldco/promptui" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" ) +const ( + newline = '\n' +) + +var ( + namePattern = regexp.MustCompile("^[\\w-]*$") + digitPattern = regexp.MustCompile("^\\d+$") +) + +func ask(question, defaults string, validator *regexp.Regexp, erratic string) (string, error) { + for { + common.Stdout("%s? %s%s %s[%s]:%s ", pretty.Green, pretty.White, question, pretty.Grey, defaults, pretty.Reset) + source := bufio.NewReader(os.Stdin) + reply, err := source.ReadString(newline) + common.Stdout("\n") + if err != nil { + return "", err + } + reply = strings.TrimSpace(reply) + if !validator.MatchString(reply) { + common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + continue + } + if len(reply) == 0 { + return defaults, nil + } + return reply, nil + } +} + +func choose(question, label string, candidates []string) (string, error) { + keys := []string{} + common.Stdout("%s%s:%s\n", pretty.Grey, label, pretty.Reset) + for index, candidate := range candidates { + key := index + 1 + keys = append(keys, fmt.Sprintf("%d", key)) + common.Stdout(" %s%2d: %s%s%s\n", pretty.Grey, key, pretty.White, candidate, pretty.Reset) + } + common.Stdout("\n") + selectable := strings.Join(keys, "|") + pattern, err := regexp.Compile(fmt.Sprintf("^(?:%s)?$", selectable)) + if err != nil { + return "", err + } + reply, err := ask(question, "1", pattern, "Give selections number from above list.") + if err != nil { + return "", err + } + selected, err := strconv.Atoi(reply) + if err != nil { + return "", err + } + return candidates[selected-1], nil +} + func Create(arguments []string) error { common.Stdout("\n") warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - prompt := promptui.Prompt{ - Label: "Give robot name", - Default: firstOf(arguments, "my-first-robot"), - Validate: hasLength, - } - robotName, err := prompt.Run() - common.Stdout("\n") + robotName, err := ask("Give robot name", firstOf(arguments, "my-first-robot"), namePattern, "Use just normal english word characters and no spaces!") if err != nil { return err @@ -37,27 +90,17 @@ func Create(arguments []string) error { return errors.New(fmt.Sprintf("Folder %s already exists. Try with other name.", robotName)) } - selection := promptui.Select{ - Label: "Choose a template", - Items: operations.ListTemplates(), - } - - _, selected, err := selection.Run() - common.Stdout("\n") - + selected, err := choose("Choose a template", "Templates", operations.ListTemplates()) if err != nil { return err } - common.Stdout("%sCreating the %s%s%s robot: %s%s%s\n", pretty.White, pretty.Cyan, selected, pretty.White, pretty.Cyan, robotName, pretty.Reset) - common.Stdout("\n") - err = operations.InitializeWorkarea(fullpath, selected, false) if err != nil { return err } - common.Stdout("%s%s%sThe %s robot has been created to: %s%s\n", pretty.Yellow, pretty.Sparkles, pretty.Green, selected, robotName, pretty.Reset) + common.Stdout("%s%s%sThe %s%s%s robot has been created to: %s%s%s\n", pretty.Yellow, pretty.Sparkles, pretty.Green, pretty.Cyan, selected, pretty.Green, pretty.Cyan, robotName, pretty.Reset) common.Stdout("\n") common.Stdout("%s%sGet started with following commands:%s\n", pretty.White, pretty.Rocket, pretty.Reset) From 9feae34b1fbbdc8a4e5acd16961feb144bbd6a18 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 23 Nov 2020 17:11:53 +0200 Subject: [PATCH 003/516] RCC-109: more robust environment creation (v6.1.0) - now environment creation will retry, when force was missing and first creation fails --- common/version.go | 2 +- conda/workflows.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 4f337d7f..9c4d42c6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.0.8` + Version = `v6.1.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 8f6e67e7..09e83206 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -93,6 +93,18 @@ func LiveExecution(liveFolder string, command ...string) error { } func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) bool { + targetFolder := LiveFrom(key) + removeClone(targetFolder) + success := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) + if !success && !force { + common.Log("Retry! First try failed ... now retrying with force!") + removeClone(targetFolder) + success = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall) + } + return success +} + +func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool) bool { targetFolder := LiveFrom(key) when := time.Now() if force { From 15d6834cc724149dd2a4ffe0939607b17ddaadbe Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Nov 2020 08:28:18 +0200 Subject: [PATCH 004/516] RCC-56: miniconda locking (v6.1.1) - new environment creation is now behind miniconda3 lock - also environment cleanup is behind that same lock - now all locks should be removed when they have done their job --- common/version.go | 2 +- conda/cleanup.go | 11 +++++++++++ conda/robocorp.go | 4 ++++ conda/workflows.go | 9 +++++++++ operations/cache.go | 2 ++ pathlib/lock_unix.go | 2 +- pathlib/lock_windows.go | 2 +- xviper/wrapper.go | 3 +++ 8 files changed, 32 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 9c4d42c6..337439b5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.0` + Version = `v6.1.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 0f2f3aba..a4f50bff 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -1,12 +1,23 @@ package conda import ( + "os" "time" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" ) func Cleanup(daylimit int, dryrun, all bool) error { + lockfile := MinicondaLock() + locker, err := pathlib.Locker(lockfile, 30000) + if err != nil { + common.Log("Could not get lock on miniconda. Quitting!") + return err + } + defer locker.Release() + defer os.Remove(lockfile) + deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) for _, template := range TemplateList() { whenLive, err := LastUsed(LiveFrom(template)) diff --git a/conda/robocorp.go b/conda/robocorp.go index 98e493ed..61603a6f 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -161,6 +161,10 @@ func TemplateLocation() string { return filepath.Join(RobocorpHome(), "base") } +func MinicondaLock() string { + return fmt.Sprintf("%s.lck", MinicondaLocation()) +} + func MinicondaLocation() string { return filepath.Join(RobocorpHome(), "miniconda3") } diff --git a/conda/workflows.go b/conda/workflows.go index 09e83206..fcf87374 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -177,6 +177,15 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s } func NewEnvironment(force bool, configurations ...string) (string, error) { + lockfile := MinicondaLock() + locker, err := pathlib.Locker(lockfile, 30000) + if err != nil { + common.Log("Could not get lock on miniconda. Quitting!") + return "", err + } + defer locker.Release() + defer os.Remove(lockfile) + requests := xviper.GetInt("stats.env.request") + 1 hits := xviper.GetInt("stats.env.hit") dirty := xviper.GetInt("stats.env.dirty") diff --git a/operations/cache.go b/operations/cache.go index 91f6ee0e..6ae8993f 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -64,6 +64,7 @@ func SummonCache() (*Cache, error) { return nil, err } defer locker.Release() + defer os.Remove(cacheLockFile()) source, err := os.Open(cacheLocation()) if err != nil { @@ -84,6 +85,7 @@ func (it *Cache) Save() error { return err } defer locker.Release() + defer os.Remove(cacheLockFile()) sink, err := os.Create(cacheLocation()) if err != nil { diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index c72d58bc..9d289156 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -35,6 +35,6 @@ func Locker(filename string, trycount int) (Releaser, error) { func (it Locked) Release() error { defer it.Close() err := syscall.Flock(int(it.Fd()), int(syscall.LOCK_UN)) - common.Trace("LOCKER: release with err: %v", err) + common.Trace("LOCKER: release %v with err: %v", it.Name(), err) return err } diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 1990b940..12c88096 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -70,7 +70,7 @@ func Locker(filename string, trycount int) (Releaser, error) { func (it Locked) Release() error { success, err := trylock(unlockFile, it) - common.Trace("LOCKER: release success: %v with err: %v", success, err) + common.Trace("LOCKER: release %v success: %v with err: %v", it.Name(), success, err) return err } diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 30eed409..0c6a7d13 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -2,6 +2,7 @@ package xviper import ( "fmt" + "os" "time" "github.com/robocorp/rcc/common" @@ -44,6 +45,7 @@ func (it *config) Save() { return } defer locker.Release() + defer os.Remove(it.Lockfile) it.Viper.WriteConfigAs(it.Filename) when, err := pathlib.Modtime(it.Filename) @@ -59,6 +61,7 @@ func (it *config) Reload() { return } defer locker.Release() + defer os.Remove(it.Lockfile) it.Viper = viper.New() it.Viper.SetConfigFile(it.Filename) From e922770a52d5227dff7fd315323624caaedba0bc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Nov 2020 08:56:31 +0200 Subject: [PATCH 005/516] RCC-56: miniconda locking (v6.1.2) - github action experiment with go version 1.15.x --- .github/workflows/rcc.yaml | 5 ++++- common/version.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index 34cb6abd..f71d2d76 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -27,13 +27,16 @@ jobs: matrix: os: ['ubuntu'] steps: - - uses: actions/checkout@v1 + - uses: actions/setup-go@v2 + with: + go-version: '1.15.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' - uses: actions/setup-python@v1 with: python-version: '3.7' + - uses: actions/checkout@v1 - name: Setup run: rake robotsetup - name: What diff --git a/common/version.go b/common/version.go index 337439b5..45839053 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.1` + Version = `v6.1.2` ) From 71c615d82cc5cead861ea0be13a72631830c044b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Nov 2020 10:16:47 +0200 Subject: [PATCH 006/516] RCC-109: more robust environment creation (v6.1.3) - added forced debug option on environment creation retry - github actions build with go 1.15.x --- .github/workflows/rcc.yaml | 8 ++++---- common/variables.go | 6 ++++++ common/version.go | 2 +- conda/workflows.go | 3 ++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index f71d2d76..ac570531 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -10,10 +10,13 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/setup-go@v2 + with: + go-version: '1.15.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' + - uses: actions/checkout@v1 - name: What run: rake what - name: Building @@ -27,9 +30,6 @@ jobs: matrix: os: ['ubuntu'] steps: - - uses: actions/setup-go@v2 - with: - go-version: '1.15.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' diff --git a/common/variables.go b/common/variables.go index 440959ee..66518d94 100644 --- a/common/variables.go +++ b/common/variables.go @@ -21,3 +21,9 @@ func UnifyVerbosityFlags() { DebugFlag = true } } + +func ForceDebug() { + Silent = false + DebugFlag = true + UnifyVerbosityFlags() +} diff --git a/common/version.go b/common/version.go index 45839053..da8337aa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.2` + Version = `v6.1.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index fcf87374..fe3b6ee8 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -97,7 +97,8 @@ func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) removeClone(targetFolder) success := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) if !success && !force { - common.Log("Retry! First try failed ... now retrying with force!") + common.ForceDebug() + common.Log("Retry! First try failed ... now retrying with debug and force options!") removeClone(targetFolder) success = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall) } From 30b72854727cfea158c0848073dcb1cd8113d207 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Nov 2020 14:12:09 +0200 Subject: [PATCH 007/516] RCC-109: more robust environment creation (v6.1.4) - checking long path support for all OSes - but only windows has meaningful handler (for cmd.exe) --- cmd/check.go | 3 +++ common/version.go | 2 +- conda/platform_darwin_amd64.go | 4 ++++ conda/platform_linux.go | 4 ++++ conda/platform_windows.go | 25 +++++++++++++++++++++++++ conda/validate.go | 4 ++++ conda/workflows.go | 3 +++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/cmd/check.go b/cmd/check.go index a6733425..7f92f450 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -18,6 +18,9 @@ conda using "rcc conda download" and "rcc conda install" commands. `, if common.DebugFlag { defer common.Stopwatch("Conda check took").Report() } + if !conda.HasLongPathSupport() { + pretty.Exit(9, "Error: does not support long paths. See above!.") + } if conda.HasConda() { pretty.Exit(0, "OK.") } diff --git a/common/version.go b/common/version.go index da8337aa..dfc4b1cd 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.3` + Version = `v6.1.4` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 2df4321c..85134ca6 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -57,6 +57,10 @@ func IsWindows() bool { return false } +func HasLongPathSupport() bool { + return true +} + func ValidateLocations() bool { return true } diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 8448e805..516350ce 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -49,6 +49,10 @@ func IsWindows() bool { return false } +func HasLongPathSupport() bool { + return true +} + func ValidateLocations() bool { return true } diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 0f03a9be..153a40a0 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -1,9 +1,14 @@ package conda import ( + "fmt" "os" "path/filepath" "regexp" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/shell" ) const ( @@ -83,3 +88,23 @@ func ValidateLocations() bool { } return validateLocations(checked) } + +func HasLongPathSupport() bool { + baseline := []string{RobocorpHome(), "stump"} + stumpath := filepath.Join(baseline...) + defer os.RemoveAll(stumpath) + + for count := 0; count < 24; count++ { + baseline = append(baseline, fmt.Sprintf("verylongpath%d", count+1)) + } + fullpath := filepath.Join(baseline...) + + code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).Transparent() + common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) + if err != nil { + common.Log("%sWARNING! Long path support failed. Reason: %v.%s", pretty.Red, err, pretty.Reset) + common.Log("%sWARNING! See %v for more details.%s", pretty.Red, longPathSupportArticle, pretty.Reset) + return false + } + return true +} diff --git a/conda/validate.go b/conda/validate.go index 592f0aa8..05506d91 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -8,6 +8,10 @@ import ( "github.com/robocorp/rcc/pretty" ) +const ( + longPathSupportArticle = `https://robocorp.com/docs/product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on` +) + var ( validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\]+$") ) diff --git a/conda/workflows.go b/conda/workflows.go index fe3b6ee8..8bcd2a96 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -93,6 +93,9 @@ func LiveExecution(liveFolder string, command ...string) error { } func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) bool { + if !HasLongPathSupport() { + return false + } targetFolder := LiveFrom(key) removeClone(targetFolder) success := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) From 2015bb4a0f21b11531bd9c4431edc71c9013de95 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 25 Nov 2020 10:39:48 +0200 Subject: [PATCH 008/516] RCC-109: more robust environment creation (v6.1.5) - made check for conda base environment corruption (and fail fast with metrics send) - moved metrics to cloud package (less due cyclic reference) --- {operations => cloud}/metrics.go | 5 ++- cmd/assistantRun.go | 10 +++--- cmd/metric.go | 4 +-- cmd/push.go | 2 +- cmd/rcc/main.go | 4 +-- cmd/root.go | 4 +-- cmd/run.go | 3 +- cmd/testrun.go | 3 +- common/version.go | 2 +- conda/workflows.go | 61 ++++++++++++++++++++++++++------ operations/credentials.go | 2 +- shell/task.go | 9 +++++ 12 files changed, 80 insertions(+), 29 deletions(-) rename {operations => cloud}/metrics.go (90%) diff --git a/operations/metrics.go b/cloud/metrics.go similarity index 90% rename from operations/metrics.go rename to cloud/metrics.go index 0f0074b6..18a41c57 100644 --- a/operations/metrics.go +++ b/cloud/metrics.go @@ -1,11 +1,10 @@ -package operations +package cloud import ( "fmt" "net/url" "time" - "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/xviper" ) @@ -16,7 +15,7 @@ const ( ) func sendMetric(kind, name, value string) { - client, err := cloud.NewClient(metricsHost) + client, err := NewClient(metricsHost) if err != nil { common.Debug("ERROR: %v", err) return diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index b14434a3..5f547687 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -46,9 +46,9 @@ var assistantRunCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } reason = "START_FAILURE" - operations.BackgroundMetric("rcc", "rcc.assistant.run.start", elapser.String()) + cloud.BackgroundMetric("rcc", "rcc.assistant.run.start", elapser.String()) defer func() { - operations.BackgroundMetric("rcc", "rcc.assistant.run.stop", reason) + cloud.BackgroundMetric("rcc", "rcc.assistant.run.stop", reason) }() assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId) if err != nil { @@ -90,7 +90,7 @@ var assistantRunCmd = &cobra.Command{ } defer func() { - operations.BackgroundMetric("rcc", "rcc.assistant.run.timeline.uploaded", elapser.String()) + cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.uploaded", elapser.String()) }() defer func() { publisher := operations.ArtifactPublisher{ @@ -106,9 +106,9 @@ var assistantRunCmd = &cobra.Command{ } }() - operations.BackgroundMetric("rcc", "rcc.assistant.run.timeline.setup", elapser.String()) + cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.setup", elapser.String()) defer func() { - operations.BackgroundMetric("rcc", "rcc.assistant.run.timeline.executed", elapser.String()) + cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.executed", elapser.String()) }() reason = "ROBOT_FAILURE" operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) diff --git a/cmd/metric.go b/cmd/metric.go index d4396088..e071fd3a 100644 --- a/cmd/metric.go +++ b/cmd/metric.go @@ -1,8 +1,8 @@ package cmd import ( + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/xviper" @@ -26,7 +26,7 @@ var metricCmd = &cobra.Command{ if !xviper.CanTrack() { pretty.Exit(1, "Tracking is disabled. Quitting.") } - operations.SendMetric(metricType, metricName, metricValue) + cloud.SendMetric(metricType, metricName, metricValue) pretty.Exit(0, "OK") }, } diff --git a/cmd/push.go b/cmd/push.go index 566d96c3..90a96b8c 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -43,7 +43,7 @@ var pushCmd = &cobra.Command{ if err != nil { pretty.Exit(4, "Error: %v", err) } - operations.BackgroundMetric("rcc", "rcc.cli.push", common.Version) + cloud.BackgroundMetric("rcc", "rcc.cli.push", common.Version) pretty.Ok() }, } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 0e17a0cf..2fcae13b 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -3,9 +3,9 @@ package main import ( "os" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" ) func ExitProtection() { @@ -16,7 +16,7 @@ func ExitProtection() { exit.ShowMessage() os.Exit(exit.Code) } - operations.SendMetric("rcc", "rcc.panic.origin", cmd.Origin()) + cloud.SendMetric("rcc", "rcc.panic.origin", cmd.Origin()) panic(status) } } diff --git a/cmd/root.go b/cmd/root.go index bc25aa75..0f415e09 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,9 +6,9 @@ import ( "path/filepath" "strings" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/xviper" @@ -99,7 +99,7 @@ func initConfig() { common.UnifyVerbosityFlags() if len(controllerType) > 0 { - operations.BackgroundMetric("rcc", "rcc.controlled.by", controllerType) + cloud.BackgroundMetric("rcc", "rcc.controlled.by", controllerType) } pretty.Setup() diff --git a/cmd/run.go b/cmd/run.go index ca5ff35a..cfe97828 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" @@ -32,7 +33,7 @@ in your own machine.`, } defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) - operations.BackgroundMetric("rcc", "rcc.cli.run", common.Version) + cloud.BackgroundMetric("rcc", "rcc.cli.run", common.Version) operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) }, } diff --git a/cmd/testrun.go b/cmd/testrun.go index 1085ccfd..2c24e57e 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -6,6 +6,7 @@ import ( "path/filepath" "time" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" @@ -58,7 +59,7 @@ var testrunCmd = &cobra.Command{ targetRobot := robot.DetectConfigurationName(workarea) simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) - operations.BackgroundMetric("rcc", "rcc.cli.testrun", common.Version) + cloud.BackgroundMetric("rcc", "rcc.cli.testrun", common.Version) operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) }, } diff --git a/common/version.go b/common/version.go index dfc4b1cd..b182382a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.4` + Version = `v6.1.5` ) diff --git a/conda/workflows.go b/conda/workflows.go index 8bcd2a96..c3e77674 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -6,10 +6,13 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "time" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/shell" "github.com/robocorp/rcc/xviper" ) @@ -92,23 +95,54 @@ func LiveExecution(liveFolder string, command ...string) error { return err } +type InstallObserver map[string]bool + +func (it InstallObserver) Write(content []byte) (int, error) { + text := strings.ToLower(string(content)) + if strings.Contains(text, "safetyerror:") { + it["safetyerror"] = true + } + if strings.Contains(text, "pkgs") { + it["pkgs"] = true + } + if strings.Contains(text, "appears to be corrupted") { + it["corrupted"] = true + } + return len(content), nil +} + +func (it InstallObserver) HasFailures(targetFolder string) bool { + if it["safetyerror"] && it["corrupted"] && len(it) > 2 { + cloud.BackgroundMetric("rcc", "rcc.env.creation.failure", common.Version) + removeClone(targetFolder) + location := filepath.Join(MinicondaLocation(), "pkgs") + common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) + common.Log("%sWARNING! To fix it, try to remove directory: %v%s", pretty.Red, location, pretty.Reset) + return true + } + return false +} + func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) bool { if !HasLongPathSupport() { return false } targetFolder := LiveFrom(key) + common.Debug("=== new live --- pre cleanup phase ===") removeClone(targetFolder) - success := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) - if !success && !force { + common.Debug("=== new live --- first try phase ===") + success, fatal := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) + if !success && !force && !fatal { + common.Debug("=== new live --- second try phase ===") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") removeClone(targetFolder) - success = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall) + success, _ = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall) } return success } -func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool) bool { +func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool) (bool, bool) { targetFolder := LiveFrom(key) when := time.Now() if force { @@ -123,27 +157,34 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal if common.DebugFlag { command = []string{CondaExecutable(), "env", "create", "-f", condaYaml, "-p", targetFolder} } - _, err := shell.New(nil, ".", command...).Transparent() - if err != nil { + observer := make(InstallObserver) + common.Debug("=== new live --- conda env create phase ===") + code, err := shell.New(nil, ".", command...).Observed(observer, false) + if err != nil || code != 0 { common.Error("Conda error", err) - return false + return false, false + } + if observer.HasFailures(targetFolder) { + return false, true } common.Debug("Updating new environment at %v with pip requirements from %v", targetFolder, requirementsText) pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", PipCache(), "--find-links", WheelCache(), "--requirement", requirementsText, "--quiet"} if common.DebugFlag { pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", PipCache(), "--find-links", WheelCache(), "--requirement", requirementsText} } + common.Debug("=== new live --- pip install phase ===") err = LiveExecution(targetFolder, pipCommand...) if err != nil { common.Error("Pip error", err) - return false + return false, false } + common.Debug("=== new live --- finalize phase ===") digest, err := DigestFor(targetFolder) if err != nil { common.Error("Digest", err) - return false + return false, false } - return metaSave(targetFolder, Hexdigest(digest)) == nil + return metaSave(targetFolder, Hexdigest(digest)) == nil, false } func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (string, error) { diff --git a/operations/credentials.go b/operations/credentials.go index 58b628a2..7939aa01 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -213,7 +213,7 @@ func loadAccount(label string) *account { } func createEphemeralAccount(parts []string) *account { - BackgroundMetric("rcc", "rcc.account.ephemeral", common.Version) + cloud.BackgroundMetric("rcc", "rcc.account.ephemeral", common.Version) common.NoCache = true endpoint := common.DefaultEndpoint if len(parts[3]) > 0 { diff --git a/shell/task.go b/shell/task.go index 00e08615..328fcbee 100644 --- a/shell/task.go +++ b/shell/task.go @@ -68,3 +68,12 @@ func (it *Task) Tee(folder string, interactive bool) (int, error) { } return it.execute(os.Stdin, stdout, stderr) } + +func (it *Task) Observed(sink io.Writer, interactive bool) (int, error) { + stdout := io.MultiWriter(os.Stdout, sink) + stderr := io.MultiWriter(os.Stderr, sink) + if !interactive { + os.Stdin.Close() + } + return it.execute(os.Stdin, stdout, stderr) +} From 02c469af8a1574f75dc5479a5b6eb025cb556ca1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 26 Nov 2020 10:26:28 +0200 Subject: [PATCH 009/516] RCC-109: more robust environment creation (v6.1.6) - trying to trigger build pipeline with new version number - minor fix on presentation of available tasks (human robustness) --- common/version.go | 2 +- operations/running.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index b182382a..e9dfc76c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.5` + Version = `v6.1.6` ) diff --git a/operations/running.go b/operations/running.go index 0c4504f5..b74f9fe1 100644 --- a/operations/running.go +++ b/operations/running.go @@ -3,6 +3,7 @@ package operations import ( "fmt" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" @@ -57,7 +58,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. todo := config.TaskByName(theTask) if todo == nil { - pretty.Exit(3, "Error: Could not resolve task to run. Available tasks are: %v", config.AvailableTasks()) + pretty.Exit(3, "Error: Could not resolve task to run. Available tasks are: %v", strings.Join(config.AvailableTasks(), ", ")) } if !config.UsesConda() { From bd5fe72e8b4eb17b0686fed6ff41fe105bdd4c93 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 27 Nov 2020 07:37:11 +0200 Subject: [PATCH 010/516] RCC-109: more robust environment creation (v6.1.7) - added --trace level printing of final conda descriptor - also in --trace, now showing both used conda.yaml and requirements.txt files content (for debugging purposes) --- common/version.go | 2 +- conda/condayaml.go | 2 ++ conda/workflows.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index e9dfc76c..5512c588 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.6` + Version = `v6.1.7` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index b40a80a3..77a01c7b 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -325,11 +325,13 @@ func (it *Environment) SaveAs(filename string) error { if err != nil { return err } + common.Trace("FINAL conda environment file as %v:\n---\n%v---", filename, content) return ioutil.WriteFile(filename, []byte(content), 0o640) } func (it *Environment) SaveAsRequirements(filename string) error { content := it.AsRequirementsText() + common.Trace("FINAL pip requirements as %v:\n---\n%v\n---", filename, content) return ioutil.WriteFile(filename, []byte(content), 0o640) } diff --git a/conda/workflows.go b/conda/workflows.go index c3e77674..e1729521 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -209,6 +209,7 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s if err != nil { return "", err } + common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash, err := LocalitySensitiveHash(AsUnifiedLines(yaml)) if err != nil { return "", err From 9b13875c858217565f62131251f1fc15716938d5 Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Fri, 27 Nov 2020 11:01:27 +0200 Subject: [PATCH 011/516] Updated tutorial.txt (v6.1.8) * Update tutorial.txt * v6.1.8 --- assets/man/tutorial.txt | 24 +++++++++++------------- common/version.go | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/assets/man/tutorial.txt b/assets/man/tutorial.txt index 8349bd78..5bfd146f 100644 --- a/assets/man/tutorial.txt +++ b/assets/man/tutorial.txt @@ -1,22 +1,20 @@ -Road to Robocorp Command Center (RCC) -===================================== +Welcome to RCC tutorial +======================= -First, download a bunch of example robots using following command. +Create you first Robot Framework or python robot and follow given instructions to run it: - rcc pull example-activities + rcc create -Then change into some robot root directory. - - cd example-activities-master/google-image-search - -Now you are ready to run your first robot. +Jumpstart your development with robot examples and templates from out community. Download and run any robot from Robocorp Portal https://robocorp.com/robots/ , for example: + rcc pull example-google-image-search + cd example-google-image-search-main rcc run -Explore other available commands. +Check help for any command with --help. Explore other available commands. rcc - rcc robot -h - rcc robot initialize -h + rcc configure credentials + rcc cloud push -Have fun! ;-) +Have fun! diff --git a/common/version.go b/common/version.go index 5512c588..effd76ac 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.7` + Version = `v6.1.8` ) From 43c0d4d28ac20d552521bb08bc9ccc5c55df563d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 27 Nov 2020 13:06:16 +0200 Subject: [PATCH 012/516] RCC-112: rcc post install scripts (v6.2.0) - in conda.yaml, there can now be additional "rccPostInstall" list of commands to execute - they are then executed in new environment creation, after conda and pip packages are installed, but before environment is calculated --- common/version.go | 2 +- conda/condayaml.go | 41 ++++++++++++++++++++++++++-------------- conda/condayaml_test.go | 1 + conda/workflows.go | 42 ++++++++++++++++++++++++++++------------- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/common/version.go b/common/version.go index effd76ac..4860118f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.1.8` + Version = `v6.2.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 77a01c7b..64eaa4a5 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -22,14 +22,16 @@ type internalEnvironment struct { Channels []string `yaml:"channels"` Dependencies []interface{} `yaml:"dependencies"` Prefix string `yaml:"prefix,omitempty"` + PostInstall []string `yaml:"rccPostInstall,omitempty"` } type Environment struct { - Name string - Prefix string - Channels []string - Conda []*Dependency - Pip []*Dependency + Name string + Prefix string + Channels []string + Conda []*Dependency + Pip []*Dependency + PostInstall []string } type Dependency struct { @@ -92,9 +94,12 @@ func (it *Dependency) Index(others []*Dependency) int { func (it *internalEnvironment) AsEnvironment() *Environment { result := &Environment{ - Name: it.Name, - Prefix: it.Prefix, + Name: it.Name, + Prefix: it.Prefix, + PostInstall: []string{}, } + seenScripts := make(map[string]bool) + result.PostInstall = addItem(seenScripts, it.PostInstall, result.PostInstall) channel, ok := LocalChannel() if ok { pushChannels(result, []string{channel}) @@ -180,12 +185,12 @@ func (it *internalEnvironment) condaDependencies() []*Dependency { return result } -func addChannels(seen map[string]bool, source, target []string) []string { - for _, channel := range source { - found := seen[channel] +func addItem(seen map[string]bool, source, target []string) []string { + for _, item := range source { + found := seen[item] if !found { - seen[channel] = true - target = append(target, channel) + seen[item] = true + target = append(target, item) } } return target @@ -220,9 +225,15 @@ func pushPip(target *Environment, dependencies []*Dependency) error { func (it *Environment) Merge(right *Environment) (*Environment, error) { result := new(Environment) result.Name = it.Name + "+" + right.Name + seenChannels := make(map[string]bool) - result.Channels = addChannels(seenChannels, it.Channels, result.Channels) - result.Channels = addChannels(seenChannels, right.Channels, result.Channels) + result.Channels = addItem(seenChannels, it.Channels, result.Channels) + result.Channels = addItem(seenChannels, right.Channels, result.Channels) + + seenScripts := make(map[string]bool) + result.PostInstall = addItem(seenScripts, it.PostInstall, result.PostInstall) + result.PostInstall = addItem(seenScripts, right.PostInstall, result.PostInstall) + err := pushConda(result, it.Conda) if err != nil { return nil, err @@ -341,6 +352,8 @@ func (it *Environment) AsYaml() (string, error) { result.Prefix = it.Prefix result.Channels = it.Channels result.Dependencies = it.CondaList() + seenScripts := make(map[string]bool) + result.PostInstall = addItem(seenScripts, it.PostInstall, result.PostInstall) if len(it.Pip) > 0 { result.Dependencies = append(result.Dependencies, it.PipMap()) } diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index a4ca109a..fc19822c 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -70,6 +70,7 @@ func TestCanCreateCondaYamlFromEmptyByteSlice(t *testing.T) { must_be.Equal(0, len(sut.Channels)) must_be.Equal(0, len(sut.Conda)) must_be.Equal(0, len(sut.Pip)) + must_be.Equal(0, len(sut.PostInstall)) } func TestCanReadCondaYaml(t *testing.T) { diff --git a/conda/workflows.go b/conda/workflows.go index e1729521..592668a6 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/google/shlex" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -123,7 +124,7 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { return false } -func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) bool { +func newLive(condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) bool { if !HasLongPathSupport() { return false } @@ -131,18 +132,18 @@ func newLive(condaYaml, requirementsText, key string, force, freshInstall bool) common.Debug("=== new live --- pre cleanup phase ===") removeClone(targetFolder) common.Debug("=== new live --- first try phase ===") - success, fatal := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall) + success, fatal := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { common.Debug("=== new live --- second try phase ===") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") removeClone(targetFolder) - success, _ = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall) + success, _ = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall, postInstall) } return success } -func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool) (bool, bool) { +func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) when := time.Now() if force { @@ -178,6 +179,21 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal common.Error("Pip error", err) return false, false } + if postInstall != nil && len(postInstall) > 0 { + common.Debug("=== new live --- post install phase ===") + for _, script := range postInstall { + scriptCommand, err := shlex.Split(script) + if err != nil { + return false, false + } + common.Log("Running post install script '%s' ...", script) + err = LiveExecution(targetFolder, scriptCommand...) + if err != nil { + common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) + return false, false + } + } + } common.Debug("=== new live --- finalize phase ===") digest, err := DigestFor(targetFolder) if err != nil { @@ -187,7 +203,7 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal return metaSave(targetFolder, Hexdigest(digest)) == nil, false } -func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (string, error) { +func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (string, *Environment, error) { var left, right *Environment var err error @@ -195,31 +211,31 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s left = right right, err = ReadCondaYaml(filename) if err != nil { - return "", err + return "", nil, err } if left == nil { continue } right, err = left.Merge(right) if err != nil { - return "", err + return "", nil, err } } yaml, err := right.AsYaml() if err != nil { - return "", err + return "", nil, err } common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash, err := LocalitySensitiveHash(AsUnifiedLines(yaml)) if err != nil { - return "", err + return "", nil, err } err = right.SaveAsRequirements(requirementsText) if err != nil { - return "", err + return "", nil, err } pure := right.AsPureConda() - return hash, pure.SaveAs(condaYaml) + return hash, right, pure.SaveAs(condaYaml) } func NewEnvironment(force bool, configurations ...string) (string, error) { @@ -257,7 +273,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", marker)) requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", marker)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) - key, err := temporaryConfig(condaYaml, requirementsText, configurations...) + key, finalEnv, err := temporaryConfig(condaYaml, requirementsText, configurations...) if err != nil { failures += 1 xviper.Set("stats.env.failures", failures) @@ -279,7 +295,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } common.Log("#### Progress: 2/4 [try create new environment from scratch]") - if newLive(condaYaml, requirementsText, key, force, freshInstall) { + if newLive(condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { misses += 1 xviper.Set("stats.env.miss", misses) if !common.Liveonly { From a5ddca5da4496198c38bcdf3cd3a12b567916913 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 27 Nov 2020 16:30:07 +0200 Subject: [PATCH 013/516] RCC-112: rcc post install scripts (v6.2.1) - added one more log entry in case of failures on post install scripts --- common/version.go | 2 +- conda/workflows.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 4860118f..f5db4058 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.2.0` + Version = `v6.2.1` ) diff --git a/conda/workflows.go b/conda/workflows.go index 592668a6..f362d41d 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -184,6 +184,7 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal for _, script := range postInstall { scriptCommand, err := shlex.Split(script) if err != nil { + common.Log("%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) return false, false } common.Log("Running post install script '%s' ...", script) From 1a1aff19c5a2423f7c685e802e5181cca17fa5eb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Nov 2020 08:22:48 +0200 Subject: [PATCH 014/516] RCC-113: error handling cleanup (v6.2.2) - was using "wrong/old" way to create errors - there is now "better" way --- common/version.go | 2 +- conda/condayaml.go | 7 +++---- conda/workflows.go | 2 +- operations/assistant.go | 12 ++++++------ operations/authorize.go | 13 ++++++------- operations/community.go | 3 +-- operations/initialize.go | 3 +-- operations/robotcache.go | 3 +-- operations/security.go | 2 +- operations/updownload.go | 10 +++++----- operations/workspaces.go | 14 +++++++------- operations/zipper.go | 3 +-- pathlib/functions.go | 3 +-- pathlib/validators.go | 3 +-- robot/robot.go | 8 ++++---- robot/setup.go | 4 ++-- wizard/create.go | 3 +-- 17 files changed, 43 insertions(+), 52 deletions(-) diff --git a/common/version.go b/common/version.go index f5db4058..ad344ecb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.2.1` + Version = `v6.2.2` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 64eaa4a5..702be87e 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -1,7 +1,6 @@ package conda import ( - "errors" "fmt" "io/ioutil" "regexp" @@ -69,7 +68,7 @@ func (it *Dependency) ExactlySame(right *Dependency) bool { func (it *Dependency) ChooseSpecific(right *Dependency) (*Dependency, error) { if !it.SameAs(right) { - return nil, errors.New(fmt.Sprintf("Not same component: %v vs. %v", it.Name, right.Name)) + return nil, fmt.Errorf("Not same component: %v vs. %v", it.Name, right.Name) } if it.IsExact() && !right.IsExact() { return it, nil @@ -80,7 +79,7 @@ func (it *Dependency) ChooseSpecific(right *Dependency) (*Dependency, error) { if it.ExactlySame(right) { return it, nil } - return nil, errors.New(fmt.Sprintf("Wont choose between dependencies: %v vs. %v", it.Original, right.Original)) + return nil, fmt.Errorf("Wont choose between dependencies: %v vs. %v", it.Original, right.Original) } func (it *Dependency) Index(others []*Dependency) int { @@ -384,7 +383,7 @@ func CondaYamlFrom(content []byte) (*Environment, error) { func ReadCondaYaml(filename string) (*Environment, error) { content, err := ioutil.ReadFile(filename) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", filename, err) } return CondaYamlFrom(content) } diff --git a/conda/workflows.go b/conda/workflows.go index f362d41d..0cac78e9 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -87,7 +87,7 @@ func LiveExecution(liveFolder string, command ...string) error { commandName := command[0] task, ok := searchPath.Which(commandName, FileExtensions) if !ok { - return errors.New(fmt.Sprintf("Cannot find command: %v", commandName)) + return fmt.Errorf("Cannot find command: %v", commandName) } common.Debug("Using %v as command %v.", task, commandName) command[0] = task diff --git a/operations/assistant.go b/operations/assistant.go index 225bcaee..32a56d0d 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -99,7 +99,7 @@ func (it *ArtifactPublisher) Publish(fullpath, relativepath string, details os.F size, ok := pathlib.Size(fullpath) if !ok { it.ErrorCount += 1 - return //errors.New(fmt.Sprintf("Could not publish file %v, reason: could not determine size!", fullpath)) + return //fmt.Errorf("Could not publish file %v, reason: could not determine size!", fullpath) } client, url, err := it.NewClient(it.ArtifactPostURL) if err != nil { @@ -193,7 +193,7 @@ func multipartUpload(url string, fields map[string]string, basename, fullpath st return err } if response.StatusCode < 200 || response.StatusCode > 299 { - return errors.New(fmt.Sprintf("Warning: status: %d reason: %s", response.StatusCode, IoAsString(response.Body))) + return fmt.Errorf("Warning: status: %d reason: %s", response.StatusCode, IoAsString(response.Body)) } return nil } @@ -228,7 +228,7 @@ func ListAssistantsCommand(client cloud.Client, account *account, workspaceId st request.Headers[authorization] = WorkspaceToken(credentials) response := client.Get(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } tokens := make([]Token, 100) err = json.Unmarshal(response.Body, &tokens) @@ -268,7 +268,7 @@ func BeatAssistantRun(client cloud.Client, account *account, workspaceId, assist } response := client.Post(request) if response.Status != 200 { - return errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return fmt.Errorf("%d: %s", response.Status, response.Body) } return nil } @@ -289,7 +289,7 @@ func StopAssistantRun(client cloud.Client, account *account, workspaceId, assist } response := client.Put(request) if response.Status != 200 { - return errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return fmt.Errorf("%d: %s", response.Status, response.Body) } return nil } @@ -311,7 +311,7 @@ func StartAssistantRun(client cloud.Client, account *account, workspaceId, assis } response := client.Post(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } plaintext, err := key.Decode(response.Body) if err != nil { diff --git a/operations/authorize.go b/operations/authorize.go index fac21e57..bfce4f5b 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" - "errors" "fmt" "strings" "time" @@ -167,15 +166,15 @@ func HmacSignature(claims *Claims, secret, nonce, bodyHash string) string { func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { account := AccountByName(accountName) if account == nil { - return nil, errors.New(fmt.Sprintf("Could not find account by name: %s", accountName)) + return nil, fmt.Errorf("Could not find account by name: %s", accountName) } client, err := cloud.NewClient(account.Endpoint) if err != nil { - return nil, errors.New(fmt.Sprintf("Could not create client for endpoint: %s reason: %s", account.Endpoint, err)) + return nil, fmt.Errorf("Could not create client for endpoint: %s reason: %w", account.Endpoint, err) } data, err := AuthorizeCommand(client, account, claims) if err != nil { - return nil, errors.New(fmt.Sprintf("Could not authorize: %s", err)) + return nil, fmt.Errorf("Could not authorize: %w", err) } return data, nil } @@ -208,7 +207,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To request.Body = strings.NewReader(body) response := client.Post(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } token := make(Token) err = json.Unmarshal(response.Body, &token) @@ -240,7 +239,7 @@ func DeleteAccount(client cloud.Client, account *account) error { request.Headers[nonceHeader] = nonce response := client.Delete(request) if response.Status < 200 || 299 < response.Status { - return errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return fmt.Errorf("%d: %s", response.Status, response.Body) } return nil } @@ -257,7 +256,7 @@ func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { request.Headers[nonceHeader] = nonce response := client.Get(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } var result UserInfo err := json.Unmarshal(response.Body, &result) diff --git a/operations/community.go b/operations/community.go index 368e49f0..8b8a9a4c 100644 --- a/operations/community.go +++ b/operations/community.go @@ -2,7 +2,6 @@ package operations import ( "crypto/sha256" - "errors" "fmt" "io" "net/http" @@ -54,7 +53,7 @@ func DownloadCommunityRobot(url, filename string) error { defer response.Body.Close() if response.StatusCode < 200 || 299 < response.StatusCode { - return errors.New(fmt.Sprintf("%s (%s)", response.Status, url)) + return fmt.Errorf("%s (%s)", response.Status, url) } out, err := os.Create(filename) diff --git a/operations/initialize.go b/operations/initialize.go index 347ddfe5..0364c7cb 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -3,7 +3,6 @@ package operations import ( "archive/zip" "bytes" - "errors" "fmt" "path/filepath" "sort" @@ -36,7 +35,7 @@ func unpack(content []byte, directory string) error { } common.Debug("Done.") if !success { - return errors.New(fmt.Sprintf("Problems while initializing robot. Use --debug to see details.")) + return fmt.Errorf("Problems while initializing robot. Use --debug to see details.") } return nil } diff --git a/operations/robotcache.go b/operations/robotcache.go index bcadf263..f79ad73a 100644 --- a/operations/robotcache.go +++ b/operations/robotcache.go @@ -1,7 +1,6 @@ package operations import ( - "errors" "fmt" "os" "path/filepath" @@ -43,7 +42,7 @@ func CacheRobot(filename string) error { } if verify != digest { defer os.Remove(target) - return errors.New(fmt.Sprintf("Could not cache %v, reason: digest mismatch.", fullpath)) + return fmt.Errorf("Could not cache %v, reason: digest mismatch.", fullpath) } go CleanupOldestRobot() return nil diff --git a/operations/security.go b/operations/security.go index ab2340a8..4680dd53 100644 --- a/operations/security.go +++ b/operations/security.go @@ -114,7 +114,7 @@ func (it *EncryptionV1) Decode(blob []byte) ([]byte, error) { return nil, err } if aesgcm.NonceSize() != len(iv) { - return nil, errors.New(fmt.Sprintf("Size difference in AES GCM nonce, %d vs. %d!", aesgcm.NonceSize(), len(iv))) + return nil, fmt.Errorf("Size difference in AES GCM nonce, %d vs. %d!", aesgcm.NonceSize(), len(iv)) } atag, err := Decoded(content.Encryption.Atag) if err != nil { diff --git a/operations/updownload.go b/operations/updownload.go index b2637b46..7930f5cf 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -56,7 +56,7 @@ func getAnyloadLink(client cloud.Client, cloudUrl, credentials string) (string, request.Headers[authorization] = BearerToken(credentials) response := client.Get(request) if response.Status != 200 { - return "", errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return "", fmt.Errorf("%d: %s", response.Status, response.Body) } token := make(Token) err := json.Unmarshal(response.Body, &token) @@ -65,11 +65,11 @@ func getAnyloadLink(client cloud.Client, cloudUrl, credentials string) (string, } uri, ok := token["uri"] if !ok { - return "", errors.New(fmt.Sprintf("Cannot find URI from %s.", response.Body)) + return "", fmt.Errorf("Cannot find URI from %s.", response.Body) } converted, ok := uri.(string) if !ok { - return "", errors.New(fmt.Sprintf("Cannot find URI as string from %s.", response.Body)) + return "", fmt.Errorf("Cannot find URI as string from %s.", response.Body) } return converted, nil } @@ -90,7 +90,7 @@ func putContent(client cloud.Client, awsUrl, zipfile string) error { request.Body = handle response := client.Put(request) if response.Status != 200 { - return errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return fmt.Errorf("%d: %s", response.Status, response.Body) } return nil } @@ -105,7 +105,7 @@ func getContent(client cloud.Client, awsUrl, zipfile string) error { request.Stream = handle response := client.Get(request) if response.Status != 200 { - return errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return fmt.Errorf("%d: %s", response.Status, response.Body) } return nil } diff --git a/operations/workspaces.go b/operations/workspaces.go index 8fd97978..49bde106 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -65,7 +65,7 @@ func WorkspacesCommand(client cloud.Client, account *account) (interface{}, erro request.Headers[authorization] = BearerToken(credentials) response := client.Get(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } tokens := make([]Token, 100) err = json.Unmarshal(response.Body, &tokens) @@ -84,7 +84,7 @@ func WorkspaceTreeCommandRequest(client cloud.Client, account *account, workspac request.Headers[authorization] = BearerToken(credentials) response := client.Get(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } return response, nil } @@ -108,7 +108,7 @@ func RobotDigestCommand(client cloud.Client, account *account, workspaceId, robo return "", err } if treedata.Robots == nil { - return "", errors.New(fmt.Sprintf("Cannot find any valid robots from workspace %v!", workspaceId)) + return "", fmt.Errorf("Cannot find any valid robots from workspace %v!", workspaceId) } var selected *RobotData = nil for _, robot := range treedata.Robots { @@ -118,15 +118,15 @@ func RobotDigestCommand(client cloud.Client, account *account, workspaceId, robo } } if selected == nil { - return "", errors.New(fmt.Sprintf("Cannot find robot %v from workspace %v!", robotId, workspaceId)) + return "", fmt.Errorf("Cannot find robot %v from workspace %v!", robotId, workspaceId) } found, ok := selected.Package["sha256"] if !ok { - return "", errors.New(fmt.Sprintf("Robot %v from workspace %v has no digest available!", robotId, workspaceId)) + return "", fmt.Errorf("Robot %v from workspace %v has no digest available!", robotId, workspaceId) } digest, ok := found.(string) if !ok { - return "", errors.New(fmt.Sprintf("Robot %v from workspace %v has no string digest available, only %#v!", robotId, workspaceId, found)) + return "", fmt.Errorf("Robot %v from workspace %v has no string digest available, only %#v!", robotId, workspaceId, found) } return digest, nil } @@ -147,7 +147,7 @@ func NewRobotCommand(client cloud.Client, account *account, workspace, robotName request.Body = strings.NewReader(body) response := client.Post(request) if response.Status != 200 { - return nil, errors.New(fmt.Sprintf("%d: %s", response.Status, response.Body)) + return nil, fmt.Errorf("%d: %s", response.Status, response.Body) } reply := make(Token) err = json.Unmarshal(response.Body, &reply) diff --git a/operations/zipper.go b/operations/zipper.go index 09c3146c..499f878b 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -2,7 +2,6 @@ package operations import ( "archive/zip" - "errors" "fmt" "io" "os" @@ -127,7 +126,7 @@ func (it *unzipper) Extract(directory string) error { } common.Debug("Done.") if !success { - return errors.New(fmt.Sprintf("Problems while unwrapping robot. Use --debug to see details.")) + return fmt.Errorf("Problems while unwrapping robot. Use --debug to see details.") } return nil } diff --git a/pathlib/functions.go b/pathlib/functions.go index 2a1469a3..ea531b5b 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -1,7 +1,6 @@ package pathlib import ( - "errors" "fmt" "os" "path/filepath" @@ -52,7 +51,7 @@ func EnsureDirectory(directory string) (string, error) { } stats, err := os.Stat(fullpath) if !stats.IsDir() { - return "", errors.New(fmt.Sprintf("Path %s is not a directory!", fullpath)) + return "", fmt.Errorf("Path %s is not a directory!", fullpath) } return fullpath, nil } diff --git a/pathlib/validators.go b/pathlib/validators.go index ebaff419..03bc4831 100644 --- a/pathlib/validators.go +++ b/pathlib/validators.go @@ -1,7 +1,6 @@ package pathlib import ( - "errors" "fmt" "os" ) @@ -27,7 +26,7 @@ func EnsureEmptyDirectory(directory string) error { } entries, err := handle.Readdir(-1) if len(entries) > 0 { - return errors.New(fmt.Sprintf("Directory %s is not empty!", fullpath)) + return fmt.Errorf("Directory %s is not empty!", fullpath) } return nil } diff --git a/robot/robot.go b/robot/robot.go index 7cca32a1..23845379 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -85,7 +85,7 @@ func (it *robot) Validate() (bool, error) { count += 1 } if count != 1 { - return false, errors.New(fmt.Sprintf("In robot.yaml, task '%s' needs exactly one of robotTaskName/shell/command definition!", name)) + return false, fmt.Errorf("In robot.yaml, task '%s' needs exactly one of robotTaskName/shell/command definition!", name) } } return true, nil @@ -295,15 +295,15 @@ func robotFrom(content []byte) (*robot, error) { func LoadRobotYaml(filename string) (Robot, error) { fullpath, err := filepath.Abs(filename) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", filename, err) } content, err := ioutil.ReadFile(fullpath) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", fullpath, err) } robot, err := robotFrom(content) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", fullpath, err) } robot.Root = filepath.Dir(fullpath) return robot, nil diff --git a/robot/setup.go b/robot/setup.go index 727e5953..f52e3f0f 100644 --- a/robot/setup.go +++ b/robot/setup.go @@ -36,11 +36,11 @@ func LoadEnvironmentSetup(filename string) (Setup, error) { } fullpath, err := filepath.Abs(filename) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", filename, err) } content, err := ioutil.ReadFile(fullpath) if err != nil { - return nil, err + return nil, fmt.Errorf("%q: %w", fullpath, err) } return EnvironmentSetupFrom(content) } diff --git a/wizard/create.go b/wizard/create.go index ed3f1c80..484887f4 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -2,7 +2,6 @@ package wizard import ( "bufio" - "errors" "fmt" "os" "path/filepath" @@ -87,7 +86,7 @@ func Create(arguments []string) error { } if pathlib.IsDir(fullpath) { - return errors.New(fmt.Sprintf("Folder %s already exists. Try with other name.", robotName)) + return fmt.Errorf("Folder %s already exists. Try with other name.", robotName) } selected, err := choose("Choose a template", "Templates", operations.ListTemplates()) From 0a88bbbc691302088050974cebb7d2a6ec905330 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Nov 2020 09:59:14 +0200 Subject: [PATCH 015/516] RCC-113: error handling cleanup (v6.2.3) - converted separated messages into Errorf forms --- cloud/client.go | 4 +--- common/version.go | 2 +- pathlib/finder.go | 7 ++----- robot/config.go | 15 +++++---------- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index 35efa961..a3f44024 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -1,7 +1,6 @@ package cloud import ( - "errors" "fmt" "io" "io/ioutil" @@ -49,8 +48,7 @@ func EnsureHttps(endpoint string) (string, error) { if strings.HasPrefix(nice, "https://") { return nice, nil } - message := fmt.Sprintf("Endpoint '%s' must start with https:// prefix.", nice) - return "", errors.New(message) + return "", fmt.Errorf("Endpoint '%s' must start with https:// prefix.", nice) } func NewClient(endpoint string) (Client, error) { diff --git a/common/version.go b/common/version.go index ad344ecb..3e17b373 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.2.2` + Version = `v6.2.3` ) diff --git a/pathlib/finder.go b/pathlib/finder.go index 750561fc..c595e80f 100644 --- a/pathlib/finder.go +++ b/pathlib/finder.go @@ -1,7 +1,6 @@ package pathlib import ( - "errors" "fmt" "os" "path/filepath" @@ -27,8 +26,7 @@ func FindNamedPath(basedir, name string) (string, error) { return result[0], nil } if len(result) > 1 { - message := fmt.Sprintf("Found %d files named as '%s'. Expecting exactly one. %s", len(result), name, result) - return emptyString, errors.New(message) + return emptyString, fmt.Errorf("Found %d files named as '%s'. Expecting exactly one. %s", len(result), name, result) } if len(pending) > 0 { pending = append(pending, emptyString) @@ -54,6 +52,5 @@ func FindNamedPath(basedir, name string) (string, error) { } } } - message := fmt.Sprintf("Could not find path named '%s'.", name) - return emptyString, errors.New(message) + return emptyString, fmt.Errorf("Could not find path named '%s'.", name) } diff --git a/robot/config.go b/robot/config.go index 3cf004d7..7a9f4767 100644 --- a/robot/config.go +++ b/robot/config.go @@ -159,24 +159,19 @@ func (it *Config) Validate() (bool, error) { } for name, activity := range it.Activities { if activity.Output == "" { - message := fmt.Sprintf("In package.yaml, 'output:' is required for activity %s!", name) - return false, errors.New(message) + return false, fmt.Errorf("In package.yaml, 'output:' is required for activity %s!", name) } if activity.Root == "" { - message := fmt.Sprintf("In package.yaml, 'activityRoot:' is required for activity %s!", name) - return false, errors.New(message) + return false, fmt.Errorf("In package.yaml, 'activityRoot:' is required for activity %s!", name) } if activity.Action == nil { - message := fmt.Sprintf("In package.yaml, 'action:' is required for activity %s!", name) - return false, errors.New(message) + return false, fmt.Errorf("In package.yaml, 'action:' is required for activity %s!", name) } if activity.Action.Command == nil { - message := fmt.Sprintf("In package.yaml, 'action/command:' is required for activity %s!", name) - return false, errors.New(message) + return false, fmt.Errorf("In package.yaml, 'action/command:' is required for activity %s!", name) } if len(activity.Action.Command) == 0 { - message := fmt.Sprintf("In package.yaml, 'action/command:' cannot be empty for activity %s!", name) - return false, errors.New(message) + return false, fmt.Errorf("In package.yaml, 'action/command:' cannot be empty for activity %s!", name) } } return true, nil From 1b4cb88782ed7c975cb4628622785e6bffde01f9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Nov 2020 10:45:58 +0200 Subject: [PATCH 016/516] RCC-114: bug fix: stderr handling (v6.2.4) --- common/version.go | 2 +- conda/workflows.go | 4 ++-- shell/task.go | 20 +++++++++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 3e17b373..79f02c4f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.2.3` + Version = `v6.2.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 0cac78e9..7012c189 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -92,7 +92,7 @@ func LiveExecution(liveFolder string, command ...string) error { common.Debug("Using %v as command %v.", task, commandName) command[0] = task environment := EnvironmentFor(liveFolder) - _, err := shell.New(environment, ".", command...).Transparent() + _, err := shell.New(environment, ".", command...).StderrOnly().Transparent() return err } @@ -160,7 +160,7 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") - code, err := shell.New(nil, ".", command...).Observed(observer, false) + code, err := shell.New(nil, ".", command...).StderrOnly().Observed(observer, false) if err != nil || code != 0 { common.Error("Conda error", err) return false, false diff --git a/shell/task.go b/shell/task.go index 328fcbee..271d5e51 100644 --- a/shell/task.go +++ b/shell/task.go @@ -12,6 +12,7 @@ type Task struct { directory string executable string args []string + stderronly bool } func New(environment []string, directory string, task ...string) *Task { @@ -21,9 +22,22 @@ func New(environment []string, directory string, task ...string) *Task { directory: directory, executable: executable, args: args, + stderronly: false, } } +func (it *Task) StderrOnly() *Task { + it.stderronly = true + return it +} + +func (it *Task) stdout() io.Writer { + if it.stderronly { + return os.Stderr + } + return os.Stdout +} + func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) { command := exec.Command(it.executable, it.args...) command.Env = it.environment @@ -43,7 +57,7 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) } func (it *Task) Transparent() (int, error) { - return it.execute(os.Stdin, os.Stdout, os.Stderr) + return it.execute(os.Stdin, it.stdout(), os.Stderr) } func (it *Task) Tee(folder string, interactive bool) (int, error) { @@ -61,7 +75,7 @@ func (it *Task) Tee(folder string, interactive bool) (int, error) { return -602, err } defer errfile.Close() - stdout := io.MultiWriter(os.Stdout, outfile) + stdout := io.MultiWriter(it.stdout(), outfile) stderr := io.MultiWriter(os.Stderr, errfile) if !interactive { os.Stdin.Close() @@ -70,7 +84,7 @@ func (it *Task) Tee(folder string, interactive bool) (int, error) { } func (it *Task) Observed(sink io.Writer, interactive bool) (int, error) { - stdout := io.MultiWriter(os.Stdout, sink) + stdout := io.MultiWriter(it.stdout(), sink) stderr := io.MultiWriter(os.Stderr, sink) if !interactive { os.Stdin.Close() From f8875e263e6b43253d097d11df944bebc7f1e741 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 1 Dec 2020 12:52:04 +0200 Subject: [PATCH 017/516] RCC-115: bug fix: short hash name (v7.0.0) - hash is now truncated sha256 (first 64 bits) - tlsh hashing is now removed from rcc, since it is not used - deleted internal lsh method (not needed anymore) - cleanup all now removes base and live directories fully - robot tests updated to new version check and new hashes --- cmd/check.go | 3 -- cmd/list.go | 2 +- cmd/lsh.go | 41 -------------------- common/version.go | 2 +- conda/cleanup.go | 10 +++++ conda/config.go | 34 ----------------- conda/config_test.go | 79 --------------------------------------- conda/workflows.go | 30 +++++---------- go.mod | 1 - go.sum | 2 - robot_tests/fullrun.robot | 6 +-- 11 files changed, 24 insertions(+), 186 deletions(-) delete mode 100644 cmd/lsh.go diff --git a/cmd/check.go b/cmd/check.go index 7f92f450..a6733425 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -18,9 +18,6 @@ conda using "rcc conda download" and "rcc conda install" commands. `, if common.DebugFlag { defer common.Stopwatch("Conda check took").Report() } - if !conda.HasLongPathSupport() { - pretty.Exit(9, "Error: does not support long paths. See above!.") - } if conda.HasConda() { pretty.Exit(0, "OK.") } diff --git a/cmd/list.go b/cmd/list.go index 3b379f69..05ca8ccf 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -23,7 +23,7 @@ in human readable form.`, pretty.Exit(1, "No environments available.") } lines := make([]string, 0, len(templates)) - common.Log("%-25s %-25s %s", "Last used", "Last cloned", "Environment (TLSH)") + common.Log("%-25s %-25s %s", "Last used", "Last cloned", "Environment") for _, template := range templates { cloned := "N/A" used := cloned diff --git a/cmd/lsh.go b/cmd/lsh.go deleted file mode 100644 index b5f1d5fb..00000000 --- a/cmd/lsh.go +++ /dev/null @@ -1,41 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var lshCmd = &cobra.Command{ - Use: "lsh", - Short: "Locality-sensitive hash calculation", - Long: `This lsh command calculates locality-sensitive hash from environment.yaml, -or requirements.txt, or similar text files.`, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - baseline := "" - failure := false - for _, arg := range args { - out, err := conda.HashConfig(arg) - if err != nil { - common.Error("lsh", err) - failure = true - continue - } - if baseline == "" { - baseline = out - } - distance, _ := conda.Distance(baseline, out) - common.Log("%s: %s <%d>", out, arg, distance) - } - if failure { - pretty.Exit(1, "Error!") - } - }, -} - -func init() { - internalCmd.AddCommand(lshCmd) -} diff --git a/common/version.go b/common/version.go index 79f02c4f..4bb2cc9e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v6.2.4` + Version = `v7.0.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index a4f50bff..311a6f26 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -41,5 +41,15 @@ func Cleanup(daylimit int, dryrun, all bool) error { RemoveEnvironment(template) common.Debug("Removed environment %v.", template) } + if all { + err = os.RemoveAll(TemplateLocation()) + if err != nil { + return err + } + err = os.RemoveAll(LiveLocation()) + if err != nil { + return err + } + } return nil } diff --git a/conda/config.go b/conda/config.go index 75696c2b..288b8baa 100644 --- a/conda/config.go +++ b/conda/config.go @@ -1,13 +1,10 @@ package conda import ( - "errors" "io/ioutil" "regexp" "sort" "strings" - - "github.com/glaslos/tlsh" ) var ( @@ -30,13 +27,6 @@ func ReadConfig(filename string) (string, error) { return string(content), nil } -func LocalitySensitiveHash(parts []string) (string, error) { - content := "==================================================\n" - content += strings.Join(parts, "\n") - result, err := tlsh.HashBytes([]byte(content)) - return result.String(), err -} - func AsUnifiedLines(value string) []string { parts := SplitLines(value) limit := len(parts) @@ -55,27 +45,3 @@ func AsUnifiedLines(value string) []string { sort.Strings(result) return result } - -func HashConfig(filename string) (string, error) { - content, err := ReadConfig(filename) - if err != nil { - return "", err - } - hash, err := LocalitySensitiveHash(AsUnifiedLines(content)) - return hash, err -} - -func Distance(left, right string) (int, error) { - if len(left) != 70 || len(left) != len(right) { - return 999999, errors.New("Incorrect length of TLSH hashes.") - } - leftish, err := tlsh.ParseStringToTlsh(left) - if err != nil { - return 0, err - } - rightish, err := tlsh.ParseStringToTlsh(right) - if err != nil { - return 0, err - } - return leftish.Diff(rightish), nil -} diff --git a/conda/config_test.go b/conda/config_test.go index 33aa1e08..9e74d0b6 100644 --- a/conda/config_test.go +++ b/conda/config_test.go @@ -54,82 +54,3 @@ func TestGetsLinesAsUnifiedCorrectly(t *testing.T) { must_be.Equal([]string{"a"}, conda.AsUnifiedLines("a\r\n\r\n")) must_be.Equal([]string{"a", "b"}, conda.AsUnifiedLines(" \r\n\tb \r\na\r\n\ta\t\r\n")) } - -func TestCanCalculateHash(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - expected := "8a900000303c000003000003030000000000033cc000000c030cf00000c03000000000" - actual, err := conda.LocalitySensitiveHash([]string{"a", "b", "c"}) - must_be.Nil(err) - wont_be.Nil(actual) - must_be.Equal(expected, actual) -} - -func TestCanCalculateHashEvenOnEmptySet(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - expected := "aa900000000c000000000000000000000000000c000000000000000000003000000000" - actual, err := conda.LocalitySensitiveHash([]string{}) - must_be.Nil(err) - wont_be.Nil(actual) - must_be.Equal(expected, actual) -} - -func TestCanCalculateHashEvenOnEmptyString(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - expected := "aa900000000c000000000000000000000000000c000000000000000000003000000000" - actual, err := conda.LocalitySensitiveHash([]string{""}) - must_be.Nil(err) - wont_be.Nil(actual) - must_be.Equal(expected, actual) -} - -func TestCanCalculateHashForConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - actual, err := conda.HashConfig("missing/bad.yaml") - wont_be.Nil(err) - must_be.Equal("", actual) - - expected := "ded08c86224cc710b22228d3a1aa1a074bdf1a44f01be819c0a816044eebb80242030a" - actual, err = conda.HashConfig("testdata/conda.yaml") - must_be.Nil(err) - must_be.Equal(expected, actual) - - other := "59c02b47324cc310a3332cc3a19a160b4bef0a04f02ff415c0f410044ddb780342030a" - actual, err = conda.HashConfig("testdata/other.yaml") - must_be.Nil(err) - must_be.Equal(other, actual) - - third := "a8d08c86224cc710b22228c3a1aa1a0b4bef1a44f01fa815c0a412044aaa780242030a" - actual, err = conda.HashConfig("testdata/third.yaml") - must_be.Nil(err) - must_be.Equal(third, actual) - - distance, err := conda.Distance(expected, other) - must_be.Nil(err) - must_be.Equal(94, distance) - - distance, err = conda.Distance(expected, third) - must_be.Nil(err) - must_be.Equal(13, distance) - - alien := "8a900000303c000003000003030000000000033cc000000c030cf00000c03000000000" - - distance, err = conda.Distance(expected, alien) - must_be.Nil(err) - must_be.Equal(384, distance) - - distance, err = conda.Distance("", alien) - wont_be.Nil(err) - must_be.Equal(999999, distance) - - distance, err = conda.Distance("iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", alien) - wont_be.Nil(err) - must_be.Equal(0, distance) - - distance, err = conda.Distance(alien, "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii") - wont_be.Nil(err) - must_be.Equal(0, distance) -} diff --git a/conda/workflows.go b/conda/workflows.go index 7012c189..db006120 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -1,6 +1,7 @@ package conda import ( + "crypto/sha256" "errors" "fmt" "io/ioutil" @@ -18,20 +19,6 @@ import ( "github.com/robocorp/rcc/xviper" ) -func chooseBestEnvironment(best int, selected, reference string, candidates []string) (int, string) { - for _, candidate := range candidates { - move, err := Distance(reference, candidate) - if err != nil { - continue - } - if move < best { - best, selected = move, candidate - } - } - - return best, selected -} - func Hexdigest(raw []byte) string { return fmt.Sprintf("%02x", raw) } @@ -125,9 +112,6 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { } func newLive(condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) bool { - if !HasLongPathSupport() { - return false - } targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") removeClone(targetFolder) @@ -227,10 +211,7 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s return "", nil, err } common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) - hash, err := LocalitySensitiveHash(AsUnifiedLines(yaml)) - if err != nil { - return "", nil, err - } + hash := shortDigest(yaml) err = right.SaveAsRequirements(requirementsText) if err != nil { return "", nil, err @@ -239,6 +220,13 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s return hash, right, pure.SaveAs(condaYaml) } +func shortDigest(content string) string { + digester := sha256.New() + digester.Write([]byte(content)) + result := Hexdigest(digester.Sum(nil)) + return result[:16] +} + func NewEnvironment(force bool, configurations ...string) (string, error) { lockfile := MinicondaLock() locker, err := pathlib.Locker(lockfile, 30000) diff --git a/go.mod b/go.mod index 9cb681fd..0572138d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.14 require ( github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284 github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.12 diff --git a/go.sum b/go.sum index 870da76f..8dd72eff 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284 h1:AWxbfoKclxMucKNN1csTKj1OqypRsbjnBtVpfMUzeuQ= -github.com/glaslos/tlsh v0.2.1-0.20190803090415-ef1954596284/go.mod h1:Fg7YBN7EUtifZmdJrQOQHvebtw5RF89IX7nWFsmaqeE= github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M= github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index d263126a..5a80bef8 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -9,7 +9,7 @@ Using and running template example with shell file Goal Show rcc version information. Step build/rcc version --controller citests - Must Have v6. + Must Have v7. Goal Show rcc license information. Step build/rcc man license --controller citests @@ -98,7 +98,7 @@ Using and running template example with shell file Goal Merge two different conda.yaml files with conflict fails Step build/rcc env new --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent - Must Have 44d08c86724dd710b33228c3a1ea1a0b4bef1a44f01fe825d0e412044aaa7c0242030a + Must Have 786f01e87dc8d6e6 Goal See variables from specific environment without robot.yaml knowledge Step build/rcc env variables --controller citests conda/testdata/conda.yaml @@ -117,7 +117,7 @@ Using and running template example with shell file Must Have PYTHONNOUSERSITE=1 Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= - Must Have ded08c86224cc710b22228d3a1aa1a074bdf1a44f01be819c0a816044eebb80242030a + Must Have f0a9e281269b31ea Goal See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml From 215a6b79c65bc6f560c67b6753dd70ffb9f59bcb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Dec 2020 07:58:09 +0200 Subject: [PATCH 018/516] RCC-115: bug fix: short hash name (v7.0.1) - added orphan cleanup for removing "unknown" files from base and live directories (behind --orphan flag) - added spotless cleanup, which will remove full directories of base, live, pipcache and miniconda3/pkgs (behind --all flag) - includes also RCC-116 related metrics changes which removes rcc.controlled.by standalone metrics and each meaningful metric will now have type of form rcc.tooling --- cmd/assistantRun.go | 10 +++--- cmd/cleanup.go | 4 ++- cmd/push.go | 2 +- cmd/rcc/main.go | 2 +- cmd/root.go | 6 +--- cmd/run.go | 2 +- cmd/sharedvariables.go | 1 - cmd/testrun.go | 2 +- common/variables.go | 20 ++++++++--- common/version.go | 2 +- conda/cleanup.go | 76 +++++++++++++++++++++++++++++++++------ conda/robocorp.go | 55 +++++++++++++++++++++++++++- conda/workflows.go | 2 +- operations/credentials.go | 2 +- 14 files changed, 151 insertions(+), 35 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 5f547687..af7c902f 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -46,9 +46,9 @@ var assistantRunCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } reason = "START_FAILURE" - cloud.BackgroundMetric("rcc", "rcc.assistant.run.start", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.String()) defer func() { - cloud.BackgroundMetric("rcc", "rcc.assistant.run.stop", reason) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) }() assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId) if err != nil { @@ -90,7 +90,7 @@ var assistantRunCmd = &cobra.Command{ } defer func() { - cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.uploaded", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.String()) }() defer func() { publisher := operations.ArtifactPublisher{ @@ -106,9 +106,9 @@ var assistantRunCmd = &cobra.Command{ } }() - cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.setup", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.setup", elapser.String()) defer func() { - cloud.BackgroundMetric("rcc", "rcc.assistant.run.timeline.executed", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.String()) }() reason = "ROBOT_FAILURE" operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 20b708ac..85ea2aaa 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -10,6 +10,7 @@ import ( var ( allFlag bool + orphanFlag bool daysOption int ) @@ -22,7 +23,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, allFlag) + err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -33,6 +34,7 @@ After cleanup, they will not be available anymore.`, func init() { envCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") + cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Cleanup all enviroments.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") } diff --git a/cmd/push.go b/cmd/push.go index 90a96b8c..8a17d29f 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -43,7 +43,7 @@ var pushCmd = &cobra.Command{ if err != nil { pretty.Exit(4, "Error: %v", err) } - cloud.BackgroundMetric("rcc", "rcc.cli.push", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.push", common.Version) pretty.Ok() }, } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 2fcae13b..5f38f1e4 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -16,7 +16,7 @@ func ExitProtection() { exit.ShowMessage() os.Exit(exit.Code) } - cloud.SendMetric("rcc", "rcc.panic.origin", cmd.Origin()) + cloud.SendMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) panic(status) } } diff --git a/cmd/root.go b/cmd/root.go index 0f415e09..fb5379af 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" @@ -77,7 +76,7 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&controllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP/rcc.yaml)") rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") @@ -98,9 +97,6 @@ func initConfig() { } common.UnifyVerbosityFlags() - if len(controllerType) > 0 { - cloud.BackgroundMetric("rcc", "rcc.controlled.by", controllerType) - } pretty.Setup() common.Trace("CLI command was: %#v", os.Args) diff --git a/cmd/run.go b/cmd/run.go index cfe97828..572724ff 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -33,7 +33,7 @@ in your own machine.`, } defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) - cloud.BackgroundMetric("rcc", "rcc.cli.run", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) }, } diff --git a/cmd/sharedvariables.go b/cmd/sharedvariables.go index b643db1c..c9f70404 100644 --- a/cmd/sharedvariables.go +++ b/cmd/sharedvariables.go @@ -16,7 +16,6 @@ var ( assistantId string bearerToken string cfgFile string - controllerType string copyDirectory string directory string endpointUrl string diff --git a/cmd/testrun.go b/cmd/testrun.go index 2c24e57e..3a17a8ba 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -59,7 +59,7 @@ var testrunCmd = &cobra.Command{ targetRobot := robot.DetectConfigurationName(workarea) simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) - cloud.BackgroundMetric("rcc", "rcc.cli.testrun", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) }, } diff --git a/common/variables.go b/common/variables.go index 66518d94..5f7bd8c6 100644 --- a/common/variables.go +++ b/common/variables.go @@ -1,11 +1,17 @@ package common +import ( + "fmt" + "strings" +) + var ( - Silent bool - DebugFlag bool - TraceFlag bool - NoCache bool - Liveonly bool + Silent bool + DebugFlag bool + TraceFlag bool + NoCache bool + Liveonly bool + ControllerType string ) const ( @@ -27,3 +33,7 @@ func ForceDebug() { DebugFlag = true UnifyVerbosityFlags() } + +func ControllerIdentity() string { + return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) +} diff --git a/common/version.go b/common/version.go index 4bb2cc9e..547cd342 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.0` + Version = `v7.0.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 311a6f26..915b2da6 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -8,7 +8,66 @@ import ( "github.com/robocorp/rcc/pathlib" ) -func Cleanup(daylimit int, dryrun, all bool) error { +func orphanCleanup(dryrun bool) error { + orphans := OrphanList() + if len(orphans) == 0 { + return nil + } + if dryrun { + common.Log("Would be removing orphans:") + for _, orphan := range orphans { + common.Log("- %v", orphan) + } + return nil + } + for _, orphan := range orphans { + var err error + if pathlib.IsDir(orphan) { + err = os.RemoveAll(orphan) + } else { + err = os.Remove(orphan) + } + if err != nil { + return err + } + common.Debug("Removed orphan %v.", orphan) + } + return nil +} + +func spotlessCleanup(dryrun bool) error { + if dryrun { + common.Log("Would be removing:") + common.Log("- %v", TemplateLocation()) + common.Log("- %v", LiveLocation()) + common.Log("- %v", PipCache()) + common.Log("- %v", CondaPackages()) + return nil + } + err := os.RemoveAll(TemplateLocation()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", TemplateLocation()) + err = os.RemoveAll(LiveLocation()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", LiveLocation()) + err = os.RemoveAll(PipCache()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", PipCache()) + err = os.RemoveAll(CondaPackages()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", CondaPackages()) + return nil +} + +func Cleanup(daylimit int, dryrun, orphans, all bool) error { lockfile := MinicondaLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -18,6 +77,10 @@ func Cleanup(daylimit int, dryrun, all bool) error { defer locker.Release() defer os.Remove(lockfile) + if all { + return spotlessCleanup(dryrun) + } + deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) for _, template := range TemplateList() { whenLive, err := LastUsed(LiveFrom(template)) @@ -41,15 +104,8 @@ func Cleanup(daylimit int, dryrun, all bool) error { RemoveEnvironment(template) common.Debug("Removed environment %v.", template) } - if all { - err = os.RemoveAll(TemplateLocation()) - if err != nil { - return err - } - err = os.RemoveAll(LiveLocation()) - if err != nil { - return err - } + if orphans { + return orphanCleanup(dryrun) } return nil } diff --git a/conda/robocorp.go b/conda/robocorp.go index 61603a6f..0d858117 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "github.com/robocorp/rcc/common" @@ -18,6 +19,7 @@ const ( var ( ignoredPaths = []string{"python", "conda"} pythonPaths = []string{"resources", "libraries", "tasks", "variables"} + hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") ) func sorted(files []os.FileInfo) { @@ -57,6 +59,19 @@ func DigestFor(folder string) ([]byte, error) { return result, nil } +func hashedEntity(name string) bool { + return hashPattern.MatchString(name) +} + +func hasDatadir(basedir, metafile string) bool { + if filepath.Ext(metafile) != ".meta" { + return false + } + fullpath := filepath.Join(basedir, metafile) + stat, err := os.Stat(fullpath[:len(fullpath)-5]) + return err == nil && stat.IsDir() +} + func hasMetafile(basedir, subdir string) bool { folder := filepath.Join(basedir, subdir) _, err := os.Stat(metafile(folder)) @@ -86,6 +101,34 @@ func dirnamesFrom(location string) []string { return result } +func orphansFrom(location string) []string { + result := make([]string, 0, 20) + handle, err := os.Open(ExpandPath(location)) + if err != nil { + common.Error("Warning", err) + return result + } + defer handle.Close() + children, err := handle.Readdir(-1) + if err != nil { + common.Error("Warning", err) + return result + } + + for _, child := range children { + hashed := hashedEntity(child.Name()) + if hashed && child.IsDir() && hasMetafile(location, child.Name()) { + continue + } + if hashed && !child.IsDir() && hasDatadir(location, child.Name()) { + continue + } + result = append(result, filepath.Join(location, child.Name())) + } + + return result +} + func FindPath(environment string) pathlib.PathParts { target := pathlib.TargetPath() target = target.Remove(ignoredPaths) @@ -132,8 +175,12 @@ func CondaExecutable() string { return ExpandPath(filepath.Join(MinicondaLocation(), "condabin", "conda")) } +func CondaPackages() string { + return ExpandPath(filepath.Join(MinicondaLocation(), "pkgs")) +} + func CondaCache() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "pkgs", "cache")) + return ExpandPath(filepath.Join(CondaPackages(), "cache")) } func HasConda() bool { @@ -214,3 +261,9 @@ func TemplateList() []string { func LiveList() []string { return dirnamesFrom(LiveLocation()) } + +func OrphanList() []string { + result := orphansFrom(TemplateLocation()) + result = append(result, orphansFrom(LiveLocation())...) + return result +} diff --git a/conda/workflows.go b/conda/workflows.go index db006120..b8eed1a3 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -101,7 +101,7 @@ func (it InstallObserver) Write(content []byte) (int, error) { func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { - cloud.BackgroundMetric("rcc", "rcc.env.creation.failure", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) removeClone(targetFolder) location := filepath.Join(MinicondaLocation(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) diff --git a/operations/credentials.go b/operations/credentials.go index 7939aa01..c8f7ffce 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -213,7 +213,7 @@ func loadAccount(label string) *account { } func createEphemeralAccount(parts []string) *account { - cloud.BackgroundMetric("rcc", "rcc.account.ephemeral", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.account.ephemeral", common.Version) common.NoCache = true endpoint := common.DefaultEndpoint if len(parts[3]) > 0 { From 790ffb2a941201c7d03a9d2570d3cc709caac41a Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Thu, 3 Dec 2020 09:16:54 +0200 Subject: [PATCH 019/516] Updated tutorial text (#6) Typo fix and linefeeds --- assets/man/tutorial.txt | 4 +++- common/version.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/man/tutorial.txt b/assets/man/tutorial.txt index 5bfd146f..4185d926 100644 --- a/assets/man/tutorial.txt +++ b/assets/man/tutorial.txt @@ -5,7 +5,9 @@ Create you first Robot Framework or python robot and follow given instructions t rcc create -Jumpstart your development with robot examples and templates from out community. Download and run any robot from Robocorp Portal https://robocorp.com/robots/ , for example: +Jumpstart your development with robot examples and templates from our community. +Download and run any robot from Robocorp Portal https://robocorp.com/robots/ +For example: rcc pull example-google-image-search cd example-google-image-search-main diff --git a/common/version.go b/common/version.go index 547cd342..cf6d1695 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.1` + Version = `v7.0.2` ) From ebb8dcec9b6739256df99df78c2715f8bfef3467 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Dec 2020 12:55:07 +0200 Subject: [PATCH 020/516] RCC-115: bug fix: short hash name (v7.0.3) - experiment support for compressing base directory - it is now disabled, but left code in place for future use - when this will be take into use it will be breaking change --- cmd/clone.go | 3 ++- common/version.go | 2 +- conda/workflows.go | 16 ++++++++-------- pathlib/copyfile.go | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/cmd/clone.go b/cmd/clone.go index be6a2a5f..7f890942 100644 --- a/cmd/clone.go +++ b/cmd/clone.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -16,7 +17,7 @@ var cloneCmd = &cobra.Command{ source := cmd.LocalFlags().Lookup("source").Value.String() target := cmd.LocalFlags().Lookup("target").Value.String() defer common.Stopwatch("rcc internal clone lasted").Report() - success := conda.CloneFromTo(source, target) + success := conda.CloneFromTo(source, target, pathlib.CopyFile) if !success { pretty.Exit(1, "Error: Cloning failed.") } diff --git a/common/version.go b/common/version.go index cf6d1695..47e43b24 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.2` + Version = `v7.0.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index b8eed1a3..a9e975bb 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -278,7 +278,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } common.Log("#### Progress: 1/4 [try clone existing same template to live, key: %v]", key) - if CloneFromTo(TemplateFrom(key), liveFolder) { + if CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) { dirty += 1 xviper.Set("stats.env.dirty", dirty) return liveFolder, nil @@ -289,7 +289,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { xviper.Set("stats.env.miss", misses) if !common.Liveonly { common.Log("#### Progress: 3/4 [backup new environment as template]") - CloneFromTo(liveFolder, TemplateFrom(key)) + CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) } else { common.Log("#### Progress: 3/4 [skipped]") } @@ -311,7 +311,7 @@ func removeClone(location string) { os.RemoveAll(location) } -func CloneFromTo(source, target string) bool { +func CloneFromTo(source, target string, copier pathlib.Copier) bool { removeClone(target) os.MkdirAll(target, 0755) @@ -323,7 +323,7 @@ func CloneFromTo(source, target string) bool { if err != nil { return false } - success := cloneFolder(source, target, 8) + success := cloneFolder(source, target, 8, copier) if !success { removeClone(target) return false @@ -338,12 +338,12 @@ func CloneFromTo(source, target string) bool { return true } -func cloneFolder(source, target string, workers int) bool { +func cloneFolder(source, target string, workers int, copier pathlib.Copier) bool { queue := make(chan copyRequest) done := make(chan bool) for x := 0; x < workers; x++ { - go copyWorker(queue, done) + go copyWorker(queue, done, copier) } success := copyFolder(source, target, queue) @@ -412,7 +412,7 @@ type copyRequest struct { source, target string } -func copyWorker(tasks chan copyRequest, done chan bool) { +func copyWorker(tasks chan copyRequest, done chan bool, copier pathlib.Copier) { for { task, ok := <-tasks if !ok { @@ -420,7 +420,7 @@ func copyWorker(tasks chan copyRequest, done chan bool) { } link, err := os.Readlink(task.source) if err != nil { - pathlib.CopyFile(task.source, task.target, false) + copier(task.source, task.target, false) continue } err = os.Symlink(link, task.target) diff --git a/pathlib/copyfile.go b/pathlib/copyfile.go index e27134e5..77044e0a 100644 --- a/pathlib/copyfile.go +++ b/pathlib/copyfile.go @@ -1,6 +1,8 @@ package pathlib import ( + "compress/flate" + "compress/gzip" "io" "os" "path/filepath" @@ -8,7 +10,41 @@ import ( "github.com/robocorp/rcc/common" ) +type copyfunc func(io.Writer, io.Reader) (int64, error) + +func archiver(target io.Writer, source io.Reader) (int64, error) { + wrapper, err := gzip.NewWriterLevel(target, flate.BestSpeed) + if err != nil { + return 0, err + } + defer wrapper.Close() + return io.Copy(wrapper, source) +} + +func restorer(target io.Writer, source io.Reader) (int64, error) { + wrapper, err := gzip.NewReader(source) + if err != nil { + return 0, err + } + defer wrapper.Close() + return io.Copy(target, wrapper) +} + +type Copier func(string, string, bool) error + +func ArchiveFile(source, target string, overwrite bool) error { + return copyFile(source, target, overwrite, archiver) +} + +func RestoreFile(source, target string, overwrite bool) error { + return copyFile(source, target, overwrite, restorer) +} + func CopyFile(source, target string, overwrite bool) error { + return copyFile(source, target, overwrite, io.Copy) +} + +func copyFile(source, target string, overwrite bool, copier copyfunc) error { targetDir := filepath.Dir(target) err := os.MkdirAll(targetDir, 0o755) if err != nil { @@ -35,7 +71,7 @@ func CopyFile(source, target string, overwrite bool) error { } defer writable.Close() - _, err = io.Copy(writable, readable) + _, err = copier(writable, readable) if err != nil { common.Error("copy-file", err) } From 74114564653697dd7e2d85672bf82ca680ef465b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 3 Dec 2020 10:56:53 +0200 Subject: [PATCH 021/516] RCC-116: some more metrics changes (v7.0.4) - added rcc.env.create.start metric with rcc version as value --- common/version.go | 2 +- conda/workflows.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 47e43b24..761e35e4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.3` + Version = `v7.0.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index a9e975bb..489ca7c8 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -228,6 +228,8 @@ func shortDigest(content string) string { } func NewEnvironment(force bool, configurations ...string) (string, error) { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) + lockfile := MinicondaLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { From b46800d3b0d3664bcf3e77aaa6692f4e0aa8f7ef Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 3 Dec 2020 16:51:06 +0200 Subject: [PATCH 022/516] RCC-117: internal hash command (v7.0.5) - added new rcc command: rcc env hash - it calculate same environment hash (64 bits) as environment creation --- cmd/envHash.go | 34 ++++++++++++++++++++++++++++++++++ common/version.go | 2 +- conda/workflows.go | 18 +++++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 cmd/envHash.go diff --git a/cmd/envHash.go b/cmd/envHash.go new file mode 100644 index 00000000..b46b2665 --- /dev/null +++ b/cmd/envHash.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var envHashCmd = &cobra.Command{ + Use: "hash ", + Short: "Calculates a hash for managed virtual environment from conda.yaml files.", + Long: "Calculates a hash for managed virtual environment from conda.yaml files.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Conda YAML hash calculation lasted").Report() + } + hash, err := conda.CalculateComboHash(args...) + if err != nil { + pretty.Exit(1, "Hash calculation failed: %v", err) + } else { + common.Log("Hash for %v is %v.", args, hash) + } + if common.Silent { + common.Stdout("%s\n", hash) + } + }, +} + +func init() { + envCmd.AddCommand(envHashCmd) +} diff --git a/common/version.go b/common/version.go index 761e35e4..57ff7581 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.4` + Version = `v7.0.5` ) diff --git a/conda/workflows.go b/conda/workflows.go index 489ca7c8..ae86485f 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -188,7 +188,7 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal return metaSave(targetFolder, Hexdigest(digest)) == nil, false } -func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (string, *Environment, error) { +func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, *Environment, error) { var left, right *Environment var err error @@ -212,12 +212,16 @@ func temporaryConfig(condaYaml, requirementsText string, filenames ...string) (s } common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash := shortDigest(yaml) + if !save { + return hash, right, nil + } err = right.SaveAsRequirements(requirementsText) if err != nil { return "", nil, err } pure := right.AsPureConda() - return hash, right, pure.SaveAs(condaYaml) + err = pure.SaveAs(condaYaml) + return hash, right, err } func shortDigest(content string) string { @@ -227,6 +231,14 @@ func shortDigest(content string) string { return result[:16] } +func CalculateComboHash(configurations ...string) (string, error) { + key, _, err := temporaryConfig("/dev/null", "/dev/null", false, configurations...) + if err != nil { + return "", err + } + return key, nil +} + func NewEnvironment(force bool, configurations ...string) (string, error) { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) @@ -264,7 +276,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", marker)) requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", marker)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) - key, finalEnv, err := temporaryConfig(condaYaml, requirementsText, configurations...) + key, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) if err != nil { failures += 1 xviper.Set("stats.env.failures", failures) From 52d05f3ae5b511899090f8a1488d85b07b93effb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Dec 2020 09:21:36 +0200 Subject: [PATCH 023/516] RCC-119: timing and timestamps (v7.0.6) - added elapsed time into cloud client requests (on trace) - added timestamping on trace level logging --- cloud/client.go | 7 +++++-- common/identities.go | 1 + common/logger.go | 16 +++++++++++++--- common/version.go | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index a3f44024..f1789cc3 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -73,10 +73,13 @@ func (it *internalClient) Endpoint() string { func (it *internalClient) does(method string, request *Request) *Response { response := new(Response) started := time.Now() + url := it.Endpoint() + request.Url + common.Trace("Doing %s %s", method, url) defer func() { - response.Elapsed = time.Now().Sub(started) + elapsed := time.Now().Sub(started) + response.Elapsed = elapsed + common.Trace("%s %s took %v", method, url, elapsed) }() - url := it.Endpoint() + request.Url httpRequest, err := http.NewRequest(method, url, request.Body) if err != nil { response.Status = 9001 diff --git a/common/identities.go b/common/identities.go index 1ca3b0ec..1669c434 100644 --- a/common/identities.go +++ b/common/identities.go @@ -20,6 +20,7 @@ func identityProvider(sink chan string) { func init() { Startup = time.Now() + Identities = make(chan string, 3) go identityProvider(Identities) } diff --git a/common/logger.go b/common/logger.go index 420ad7c7..098b3efc 100644 --- a/common/logger.go +++ b/common/logger.go @@ -2,9 +2,19 @@ package common import ( "fmt" + "io" "os" + "time" ) +func printout(out io.Writer, message string) { + var stamp string + if TraceFlag { + stamp = time.Now().Format("02.150405.000 ") + } + fmt.Fprintf(out, "%s%s\n", stamp, message) +} + func Error(context string, err error) { if err != nil { Log("Error [%s]: %v", context, err) @@ -13,14 +23,14 @@ func Error(context string, err error) { func Log(format string, details ...interface{}) { if !Silent { - fmt.Fprintln(os.Stderr, fmt.Sprintf(format, details...)) + printout(os.Stderr, fmt.Sprintf(format, details...)) os.Stderr.Sync() } } func Debug(format string, details ...interface{}) error { if DebugFlag { - fmt.Fprintln(os.Stderr, fmt.Sprintf(format, details...)) + printout(os.Stderr, fmt.Sprintf(format, details...)) os.Stderr.Sync() } return nil @@ -28,7 +38,7 @@ func Debug(format string, details ...interface{}) error { func Trace(format string, details ...interface{}) error { if TraceFlag { - fmt.Fprintln(os.Stderr, fmt.Sprintf(format, details...)) + printout(os.Stderr, fmt.Sprintf(format, details...)) os.Stderr.Sync() } return nil diff --git a/common/version.go b/common/version.go index 57ff7581..fdf3fd2c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.5` + Version = `v7.0.6` ) From 5d54d70d6a5e35bb5c70a5e3cd71dc698dc0c25f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 9 Dec 2020 14:02:48 +0200 Subject: [PATCH 024/516] RCC-120: longpath check and fix for windows (v7.0.7) - added new command: rcc configure longpaths - added windows specific functionality for registry editing - but there are empty stubs for Mac and Linux, so that command can be executed on all operating systems --- cmd/longpaths.go | 35 ++++++++++++++++++++++++++++++++++ common/version.go | 2 +- conda/platform_darwin_amd64.go | 4 ++++ conda/platform_linux.go | 4 ++++ conda/platform_windows.go | 11 +++++++++++ go.mod | 2 +- go.sum | 2 ++ 7 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 cmd/longpaths.go diff --git a/cmd/longpaths.go b/cmd/longpaths.go new file mode 100644 index 00000000..e9efdd30 --- /dev/null +++ b/cmd/longpaths.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + enableLongpaths bool +) + +var longpathsCmd = &cobra.Command{ + Use: "longpaths", + Short: "Check and enable Windows longpath support", + Long: "Check and enable Windows longpath support", + Run: func(cmd *cobra.Command, args []string) { + var err error + if enableLongpaths { + err = conda.EnforceLongpathSupport() + } + if err != nil { + pretty.Exit(1, "Failure to modify registry: %v", err) + } + if !conda.HasLongPathSupport() { + pretty.Exit(2, "Long paths do not work!") + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(longpathsCmd) + longpathsCmd.Flags().BoolVarP(&enableLongpaths, "enable", "e", false, "Change registry settings and enable longpath support") +} diff --git a/common/version.go b/common/version.go index fdf3fd2c..81fd5286 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.6` + Version = `v7.0.7` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 85134ca6..3f910ebf 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -64,3 +64,7 @@ func HasLongPathSupport() bool { func ValidateLocations() bool { return true } + +func EnforceLongpathSupport() error { + return nil +} diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 516350ce..ec692169 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -56,3 +56,7 @@ func HasLongPathSupport() bool { func ValidateLocations() bool { return true } + +func EnforceLongpathSupport() error { + return nil +} diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 153a40a0..17c24db9 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -6,6 +6,8 @@ import ( "path/filepath" "regexp" + "golang.org/x/sys/windows/registry" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/shell" @@ -108,3 +110,12 @@ func HasLongPathSupport() bool { } return true } + +func EnforceLongpathSupport() error { + key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\FileSystem`, registry.SET_VALUE) + if err != nil { + return err + } + defer key.Close() + return key.SetDWordValue("LongPathsEnabled", 1) +} diff --git a/go.mod b/go.mod index 0572138d..76d838fc 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.6.2 - golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect + golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/yaml.v2 v2.2.8 diff --git a/go.sum b/go.sum index 8dd72eff..a2eb5720 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= From 669cd689f9850b56d92d57b518976554a5a570a2 Mon Sep 17 00:00:00 2001 From: Henrik Vesterinen Date: Mon, 7 Dec 2020 16:53:16 +0200 Subject: [PATCH 025/516] RCC-121: update templates (v7.0.8) * Mostly overhauls the extended template, makes it simpler. --- common/version.go | 2 +- templates/extended/.gitignore | 0 templates/extended/LICENSE | 1 - templates/extended/README.md | 65 ++++++++++++++++++- templates/extended/TODO.md | 38 ----------- templates/extended/bin/INTRODUCTION.md | 7 -- templates/extended/{config => }/conda.yaml | 0 templates/extended/config/INTRODUCTION.md | 14 ---- templates/extended/devdata/INTRODUCTION.md | 12 ---- templates/extended/devdata/env.json | 4 +- templates/extended/keywords/keywords.robot | 7 ++ .../extended/libraries/ExampleLibrary.py | 6 -- templates/extended/libraries/INTRODUCTION.md | 5 -- templates/extended/libraries/Library.py | 8 +++ templates/extended/resources/INTRODUCTION.md | 5 -- templates/extended/resources/keywords.robot | 12 ---- templates/extended/robot.yaml | 11 ++-- templates/extended/tasks.robot | 11 ++++ templates/extended/tasks/INTRODUCTION.md | 5 -- templates/extended/tasks/tasks.robot | 7 -- templates/extended/variables/INTRODUCTION.md | 5 -- templates/extended/variables/__init__.py | 0 templates/extended/variables/variables.py | 8 +-- templates/python/task.py | 2 +- templates/standard/tasks.robot | 3 +- 25 files changed, 101 insertions(+), 137 deletions(-) mode change 100644 => 100755 templates/extended/.gitignore delete mode 100644 templates/extended/LICENSE delete mode 100644 templates/extended/TODO.md delete mode 100644 templates/extended/bin/INTRODUCTION.md rename templates/extended/{config => }/conda.yaml (100%) delete mode 100644 templates/extended/config/INTRODUCTION.md delete mode 100644 templates/extended/devdata/INTRODUCTION.md create mode 100644 templates/extended/keywords/keywords.robot delete mode 100644 templates/extended/libraries/ExampleLibrary.py delete mode 100644 templates/extended/libraries/INTRODUCTION.md create mode 100644 templates/extended/libraries/Library.py delete mode 100644 templates/extended/resources/INTRODUCTION.md delete mode 100644 templates/extended/resources/keywords.robot mode change 100644 => 100755 templates/extended/robot.yaml create mode 100644 templates/extended/tasks.robot delete mode 100644 templates/extended/tasks/INTRODUCTION.md delete mode 100644 templates/extended/tasks/tasks.robot delete mode 100644 templates/extended/variables/INTRODUCTION.md create mode 100644 templates/extended/variables/__init__.py diff --git a/common/version.go b/common/version.go index 81fd5286..306f4857 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.7` + Version = `v7.0.8` ) diff --git a/templates/extended/.gitignore b/templates/extended/.gitignore old mode 100644 new mode 100755 diff --git a/templates/extended/LICENSE b/templates/extended/LICENSE deleted file mode 100644 index 9c312761..00000000 --- a/templates/extended/LICENSE +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/extended/README.md b/templates/extended/README.md index f1e07425..0070b9dd 100644 --- a/templates/extended/README.md +++ b/templates/extended/README.md @@ -1,3 +1,64 @@ -# README for this robot +# README for the robot - +Describe your robot here. +E.g. what it does, what are the requirements, how to run it. + + +## Development guide + +Run the robot locally: +``` +rcc run +``` + +Provide access credentials for Robocorp Cloud connectivity: +``` +rcc configure credentials +``` + +Upload to Robocorp Cloud: +``` +rcc cloud push --workspace --robot +``` + +### Suggested directory structure + +The directory structure given by the template: +``` +├── devdata +├── keywords +│ └── keywords.robot +├── libraries +│ └── Library.py +├── variables +│ └── variables.py +├── conda.yaml +├── robot.yaml +└── tasks.robot +``` + +where +* `devdata`: place for all data/material related to development, e.g. test data. Do not put any sensitive data here! +* `keywords`: Robot Framework keyword files. +* `libraries`: Python library code. +* `variables`: Define your robot variables in a centralized place. Do not put any sensitive data here! +* `conda.yaml`: Environment configuration file. +* `robot.yaml`: Robot configuration file. +* `tasks.robot`: Robot Framework task suite - high level process definition. + +In addition to these you can create your own directories (e.g. `bin`, `tmp`). Add these directories to the `PATH` or `PYTHONPATH` section of `robot.yaml` if necessary. + +Logs and artifacts are stored in `output` by default - see `robot.yaml` for configuring this. + +See [Docs](https://robocorp.com/docs/development-howtos/variables-and-secrets/) for handling variables and secrets. + + +### Configuration + +Give the task name and startup commands in `robot.yaml` with some additional configuration. See [Docs](https://robocorp.com/docs/setup/robot-structure#robot-configuration-file-robot-yaml) for more. + + +Put all the robot dependencies in `conda.yaml`. Robocorp App (and rcc) uses [Conda](https://docs.conda.io) for managing the execution environment. For development you can also install packages manually with `pip`. + +### Additional documentation +See [Robocorp Docs](https://robocorp.com/docs/) for more documentation. diff --git a/templates/extended/TODO.md b/templates/extended/TODO.md deleted file mode 100644 index 722a1f8a..00000000 --- a/templates/extended/TODO.md +++ /dev/null @@ -1,38 +0,0 @@ -# TODO file for a new robot - -- Read all the `INTRODUCTION.md` files and follow their guidance (the files - should help you get an understanding of what belongs where in this new - robot). -- Add your content to the top-level `README.md` file. -- Add your license text to the `LICENSE` file. -- Make your changes to this project and make it yours. -- Refer to the "Full workflow with CLI" below for running your task. - -# Full workflow with CLI - -In the shell (or the command prompt), do the following: - -## Creating a new task - -```bash -rcc robot init --directory new_robot -cd new_robot -``` - -## Running the task in place - -``` -rcc task run --robot robot.yaml -``` - -## Providing access credentials for Robocorp Cloud connectivity - -```bash -rcc configure credentials -``` - -## Uploading to Robocorp Cloud - -```bash -rcc cloud push --workspace 111 --robot 111 -``` diff --git a/templates/extended/bin/INTRODUCTION.md b/templates/extended/bin/INTRODUCTION.md deleted file mode 100644 index 356d8f54..00000000 --- a/templates/extended/bin/INTRODUCTION.md +++ /dev/null @@ -1,7 +0,0 @@ -# Adding executable files - -If you need your robot to have standalone binaries or executables, you -should add them to this directory. - -This directory will be in PATH when the robot is executed in Robocorp App -or through Robocorp CLI. diff --git a/templates/extended/config/conda.yaml b/templates/extended/conda.yaml similarity index 100% rename from templates/extended/config/conda.yaml rename to templates/extended/conda.yaml diff --git a/templates/extended/config/INTRODUCTION.md b/templates/extended/config/INTRODUCTION.md deleted file mode 100644 index 38a19815..00000000 --- a/templates/extended/config/INTRODUCTION.md +++ /dev/null @@ -1,14 +0,0 @@ -# Dependencies (Python packages) - -If your robot project requires dependencies such as Python packages, -you should define them in the `conda.yaml` file in this directory. Robocorp -App will use `conda.yaml` file to set up a conda environment when executed -in a target environment. - -For a local environment, you can use `pip` or `conda` or another preferred -package manager to install the dependencies. Keep the `conda.yaml` in sync with -the required dependencies. Otherwise, the robot might work when run locally, -but not when run using Robocorp App. - -If you do not want to use conda at all and want to provide the execution -environment yourself, you can delete the `conda.yaml` file. diff --git a/templates/extended/devdata/INTRODUCTION.md b/templates/extended/devdata/INTRODUCTION.md deleted file mode 100644 index bc550df1..00000000 --- a/templates/extended/devdata/INTRODUCTION.md +++ /dev/null @@ -1,12 +0,0 @@ -# Devdata placeholder - -This directory should contain local development data. - -This data should not be used in actual Robocorp App run but can be -used when you are building and debugging your robots and tasks. - -## What to put here? - -- Local work item data -- Local environment variables (in JSON format) -- Local custom data files (*.csv, etc.) diff --git a/templates/extended/devdata/env.json b/templates/extended/devdata/env.json index e3cd832d..231725b1 100644 --- a/templates/extended/devdata/env.json +++ b/templates/extended/devdata/env.json @@ -1,5 +1,3 @@ { - "GREETINGS": "Hello", - "WHO_TO_GREET": "World", - "THE_ANSWER": 42 + "SOME_DEV_ENV_URL": "https://robocorp.com/docs/development-howtos/variables-and-secrets/" } diff --git a/templates/extended/keywords/keywords.robot b/templates/extended/keywords/keywords.robot new file mode 100644 index 00000000..11941193 --- /dev/null +++ b/templates/extended/keywords/keywords.robot @@ -0,0 +1,7 @@ +*** Settings *** +Documentation Template keyword resource. + + +*** Keywords *** +Example keyword + Log Today is ${TODAY} diff --git a/templates/extended/libraries/ExampleLibrary.py b/templates/extended/libraries/ExampleLibrary.py deleted file mode 100644 index 7036f665..00000000 --- a/templates/extended/libraries/ExampleLibrary.py +++ /dev/null @@ -1,6 +0,0 @@ -from datetime import date - - -class ExampleLibrary: - def current_date(self): - return date.today() diff --git a/templates/extended/libraries/INTRODUCTION.md b/templates/extended/libraries/INTRODUCTION.md deleted file mode 100644 index 11fd3458..00000000 --- a/templates/extended/libraries/INTRODUCTION.md +++ /dev/null @@ -1,5 +0,0 @@ -# Libraries - -Place your libraries in this directory. - -This directory will be inserted into PYTHONPATH environment variable. diff --git a/templates/extended/libraries/Library.py b/templates/extended/libraries/Library.py new file mode 100644 index 00000000..e319ecbf --- /dev/null +++ b/templates/extended/libraries/Library.py @@ -0,0 +1,8 @@ +from robot.api import logger + + +class Library: + """Give this library a proper name and document it.""" + + def example_python_keyword(self): + logger.info("This is Python!") diff --git a/templates/extended/resources/INTRODUCTION.md b/templates/extended/resources/INTRODUCTION.md deleted file mode 100644 index 2c53f682..00000000 --- a/templates/extended/resources/INTRODUCTION.md +++ /dev/null @@ -1,5 +0,0 @@ -# Robot Resources - -Place your Robot Framework keyword files (`*.robot`) in this directory. - -This directory will be inserted into PYTHONPATH environment variable. diff --git a/templates/extended/resources/keywords.robot b/templates/extended/resources/keywords.robot deleted file mode 100644 index 689283e4..00000000 --- a/templates/extended/resources/keywords.robot +++ /dev/null @@ -1,12 +0,0 @@ -*** Settings *** -Library ExampleLibrary -Variables variables.py - -*** Keywords *** -Process greeting for today - ${current_date}= Current date - ${day_name}= Get week day name - Log Hello! Today is ${day_name}. The date is ${current_date}. console=True - -Get week day name - [Return] ${WEEK_DAY_NAME} diff --git a/templates/extended/robot.yaml b/templates/extended/robot.yaml old mode 100644 new mode 100755 index f40cabe1..664eaa66 --- a/templates/extended/robot.yaml +++ b/templates/extended/robot.yaml @@ -10,16 +10,15 @@ tasks: - output - --logtitle - Task log - - tasks/ + - tasks.robot -condaConfigFile: config/conda.yaml +condaConfigFile: conda.yaml ignoreFiles: - .gitignore artifactsDir: output PATH: - - bin - - entrypoints + - . PYTHONPATH: - - variables + - keywords - libraries - - resources + - variables diff --git a/templates/extended/tasks.robot b/templates/extended/tasks.robot new file mode 100644 index 00000000..5eb52e57 --- /dev/null +++ b/templates/extended/tasks.robot @@ -0,0 +1,11 @@ +*** Settings *** +Documentation Template robot main suite. +Resource keywords.robot +Library Library.py +Variables variables.py + + +*** Tasks *** +Example task + Example Keyword + Example Python Keyword diff --git a/templates/extended/tasks/INTRODUCTION.md b/templates/extended/tasks/INTRODUCTION.md deleted file mode 100644 index 54ec77ba..00000000 --- a/templates/extended/tasks/INTRODUCTION.md +++ /dev/null @@ -1,5 +0,0 @@ -# Robot tasks - -Place your Robot Framework task files (`*.robot`) in this directory. - -This directory will be inserted into PYTHONPATH environment variable. diff --git a/templates/extended/tasks/tasks.robot b/templates/extended/tasks/tasks.robot deleted file mode 100644 index 3c2b5a9f..00000000 --- a/templates/extended/tasks/tasks.robot +++ /dev/null @@ -1,7 +0,0 @@ -*** Settings *** -Documentation An example robot. -Resource keywords.robot - -*** Tasks *** -Log greeting for today - Process greeting for today diff --git a/templates/extended/variables/INTRODUCTION.md b/templates/extended/variables/INTRODUCTION.md deleted file mode 100644 index 509839f4..00000000 --- a/templates/extended/variables/INTRODUCTION.md +++ /dev/null @@ -1,5 +0,0 @@ -# Variables - -Place your variable files (`*.py`) in this directory. - -This directory will be inserted into PYTHONPATH environment variable. diff --git a/templates/extended/variables/__init__.py b/templates/extended/variables/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/templates/extended/variables/variables.py b/templates/extended/variables/variables.py index ca0fb586..d398aff1 100644 --- a/templates/extended/variables/variables.py +++ b/templates/extended/variables/variables.py @@ -1,7 +1,3 @@ -''' -Variables for Robot Framework goes here. -''' -import calendar -from datetime import date +from datetime import datetime -WEEK_DAY_NAME = calendar.day_name[date.today().weekday()] +TODAY = datetime.now() diff --git a/templates/python/task.py b/templates/python/task.py index c08134a8..0919c9d0 100644 --- a/templates/python/task.py +++ b/templates/python/task.py @@ -1,4 +1,4 @@ -""" An example robot. """ +"""Template robot with Python.""" def minimal_task(): diff --git a/templates/standard/tasks.robot b/templates/standard/tasks.robot index 6b74ec9e..4903b4cf 100644 --- a/templates/standard/tasks.robot +++ b/templates/standard/tasks.robot @@ -1,5 +1,6 @@ *** Settings *** -Documentation An example robot. +Documentation Template robot main suite. + *** Tasks *** Minimal task From 376f2d1946c316e10501e5152f41b6871c0213b0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 11 Dec 2020 16:16:05 +0200 Subject: [PATCH 026/516] RCC-121: update templates (v7.0.9) - fix to robot tests --- common/version.go | 2 +- robot_tests/fullrun.robot | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 306f4857..e6f8da2f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.8` + Version = `v7.0.9` ) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 5a80bef8..ebb9b652 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -127,7 +127,6 @@ Using and running template example with shell file Step build/rcc env variables --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= - Must Have THE_ANSWER=42 Must Have CONDA_DEFAULT_ENV=rcc Must Have CONDA_EXE= Must Have CONDA_PREFIX= From 2caeab513f03996b545eacf10060b6fce025b4b7 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 11 Dec 2020 14:31:32 +0200 Subject: [PATCH 027/516] RCC-123: micromamba PoC (v7.0.10) - partial hardlink implementation (but using only conda env now) - micromamba PoC additions (ROBOCORP_HOME/bin/micromamba) --- common/version.go | 2 +- conda/installing.go | 2 +- conda/platform_darwin_amd64.go | 11 +++++++++++ conda/platform_linux.go | 11 +++++++++++ conda/platform_windows.go | 22 ++++++++++++++++++++++ conda/robocorp.go | 11 +++++++++++ conda/workflows.go | 5 ++++- 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index e6f8da2f..0090eb67 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.9` + Version = `v7.0.10` ) diff --git a/conda/installing.go b/conda/installing.go index 53342e44..a01b19cd 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -39,7 +39,7 @@ func DoInstall() bool { install := InstallCommand() common.Debug("Running: %v", install) - _, err := shell.New(nil, ".", install...).Transparent() + _, err := shell.New(CondaEnvironment(), ".", install...).Transparent() if err != nil { common.Error("Install", err) return false diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 3f910ebf..6c053f41 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -1,6 +1,7 @@ package conda import ( + "fmt" "os" "path/filepath" ) @@ -25,6 +26,16 @@ func ExpandPath(entry string) string { return result } +func CondaEnvironment() []string { + env := os.Environ() + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + return env +} + +func BinMicromamba() string { + return ExpandPath(filepath.Join(BinLocation(), "micromamba")) +} + func BinConda() string { return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "conda")) } diff --git a/conda/platform_linux.go b/conda/platform_linux.go index ec692169..c1835483 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -1,6 +1,7 @@ package conda import ( + "fmt" "os" "path/filepath" ) @@ -25,6 +26,16 @@ func ExpandPath(entry string) string { return result } +func CondaEnvironment() []string { + env := os.Environ() + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + return env +} + +func BinMicromamba() string { + return ExpandPath(filepath.Join(BinLocation(), "micromamba")) +} + func BinConda() string { return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "conda")) } diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 17c24db9..36e12a7c 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -50,6 +50,28 @@ func ExpandPath(entry string) string { return result } +func ensureHardlinkEnvironmment() (string, error) { + return "", fmt.Errorf("Not implemented yet!") +} + +func CondaEnvironment() []string { + env := os.Environ() + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + /* + path, err := ensureHardlinkEnvironmment() + if err != nil { + return nil + } + env = append(env, fmt.Sprintf("TEMP=%s", path)) + env = append(env, fmt.Sprintf("TMP=%s", path)) + */ + return env +} + +func BinMicromamba() string { + return ExpandPath(filepath.Join(BinLocation(), "micromamba.exe")) +} + func BinConda() string { return ExpandPath(filepath.Join(MinicondaLocation(), "Scripts", "conda.exe")) } diff --git a/conda/robocorp.go b/conda/robocorp.go index 0d858117..8cd842cf 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -184,6 +184,9 @@ func CondaCache() string { } func HasConda() bool { + if HasMicroMamba() { + return true + } location := ExpandPath(filepath.Join(MinicondaLocation(), "condabin")) stat, err := os.Stat(location) if err == nil && stat.IsDir() { @@ -192,6 +195,10 @@ func HasConda() bool { return false } +func HasMicroMamba() bool { + return pathlib.IsFile(BinMicromamba()) +} + func RobocorpHome() string { home := os.Getenv(ROBOCORP_HOME_VARIABLE) if len(home) > 0 { @@ -200,6 +207,10 @@ func RobocorpHome() string { return ExpandPath(defaultRobocorpLocation) } +func BinLocation() string { + return filepath.Join(RobocorpHome(), "bin") +} + func LiveLocation() string { return filepath.Join(RobocorpHome(), "live") } diff --git a/conda/workflows.go b/conda/workflows.go index ae86485f..d6ef1719 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -142,9 +142,12 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal if common.DebugFlag { command = []string{CondaExecutable(), "env", "create", "-f", condaYaml, "-p", targetFolder} } + if HasMicroMamba() { + command = []string{BinMicromamba(), "create", "-y", "-f", condaYaml, "-p", targetFolder} + } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") - code, err := shell.New(nil, ".", command...).StderrOnly().Observed(observer, false) + code, err := shell.New(CondaEnvironment(), ".", command...).StderrOnly().Observed(observer, false) if err != nil || code != 0 { common.Error("Conda error", err) return false, false From 515403151171a43ae6223c8ac7259dd8b99f01db Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 16 Dec 2020 09:38:00 +0200 Subject: [PATCH 028/516] RCC-122: identity.yaml to environment root (v7.1.0) - now conda.yaml is saved into environment as identity.yaml - also micromamba pkgs are part of cleanup process now --- common/version.go | 2 +- conda/cleanup.go | 6 ++++++ conda/robocorp.go | 4 ++++ conda/workflows.go | 35 +++++++++++++++++++++-------------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/common/version.go b/common/version.go index 0090eb67..52f97a2b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.0.10` + Version = `v7.1.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 915b2da6..6a1be7cd 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -42,6 +42,7 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", LiveLocation()) common.Log("- %v", PipCache()) common.Log("- %v", CondaPackages()) + common.Log("- %v", MambaPackages()) return nil } err := os.RemoveAll(TemplateLocation()) @@ -64,6 +65,11 @@ func spotlessCleanup(dryrun bool) error { return err } common.Debug("Removed directory %v.", CondaPackages()) + err = os.RemoveAll(MambaPackages()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", MambaPackages()) return nil } diff --git a/conda/robocorp.go b/conda/robocorp.go index 8cd842cf..ec9c4d1a 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -175,6 +175,10 @@ func CondaExecutable() string { return ExpandPath(filepath.Join(MinicondaLocation(), "condabin", "conda")) } +func MambaPackages() string { + return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) +} + func CondaPackages() string { return ExpandPath(filepath.Join(MinicondaLocation(), "pkgs")) } diff --git a/conda/workflows.go b/conda/workflows.go index d6ef1719..fe316cdc 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -111,23 +111,23 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { return false } -func newLive(condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) bool { +func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) bool { targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") removeClone(targetFolder) common.Debug("=== new live --- first try phase ===") - success, fatal := newLiveInternal(condaYaml, requirementsText, key, force, freshInstall, postInstall) + success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { common.Debug("=== new live --- second try phase ===") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") removeClone(targetFolder) - success, _ = newLiveInternal(condaYaml, requirementsText, key, true, freshInstall, postInstall) + success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, postInstall) } return success } -func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { +func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) when := time.Now() if force { @@ -183,6 +183,13 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal } } common.Debug("=== new live --- finalize phase ===") + + markerFile := filepath.Join(targetFolder, "identity.yaml") + err = ioutil.WriteFile(markerFile, []byte(yaml), 0o640) + if err != nil { + return false, false + } + digest, err := DigestFor(targetFolder) if err != nil { common.Error("Digest", err) @@ -191,7 +198,7 @@ func newLiveInternal(condaYaml, requirementsText, key string, force, freshInstal return metaSave(targetFolder, Hexdigest(digest)) == nil, false } -func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, *Environment, error) { +func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { var left, right *Environment var err error @@ -199,32 +206,32 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. left = right right, err = ReadCondaYaml(filename) if err != nil { - return "", nil, err + return "", "", nil, err } if left == nil { continue } right, err = left.Merge(right) if err != nil { - return "", nil, err + return "", "", nil, err } } yaml, err := right.AsYaml() if err != nil { - return "", nil, err + return "", "", nil, err } common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash := shortDigest(yaml) if !save { - return hash, right, nil + return hash, yaml, right, nil } err = right.SaveAsRequirements(requirementsText) if err != nil { - return "", nil, err + return "", "", nil, err } pure := right.AsPureConda() err = pure.SaveAs(condaYaml) - return hash, right, err + return hash, yaml, right, err } func shortDigest(content string) string { @@ -235,7 +242,7 @@ func shortDigest(content string) string { } func CalculateComboHash(configurations ...string) (string, error) { - key, _, err := temporaryConfig("/dev/null", "/dev/null", false, configurations...) + key, _, _, err := temporaryConfig("/dev/null", "/dev/null", false, configurations...) if err != nil { return "", err } @@ -279,7 +286,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", marker)) requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", marker)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) - key, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) + key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) if err != nil { failures += 1 xviper.Set("stats.env.failures", failures) @@ -301,7 +308,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } common.Log("#### Progress: 2/4 [try create new environment from scratch]") - if newLive(condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { + if newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { misses += 1 xviper.Set("stats.env.miss", misses) if !common.Liveonly { From 5f0ff213028f941d32032be9fa53b7cfc0688e29 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 17 Dec 2020 12:44:39 +0200 Subject: [PATCH 029/516] OTHER: indentation fixes in robot.yaml files (v7.1.1) --- common/version.go | 2 +- templates/python/robot.yaml | 2 +- templates/standard/robot.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 52f97a2b..50b568df 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.0` + Version = `v7.1.1` ) diff --git a/templates/python/robot.yaml b/templates/python/robot.yaml index 5d3610ca..56b9080a 100644 --- a/templates/python/robot.yaml +++ b/templates/python/robot.yaml @@ -11,4 +11,4 @@ PATH: PYTHONPATH: - . ignoreFiles: - - .gitignore + - .gitignore diff --git a/templates/standard/robot.yaml b/templates/standard/robot.yaml index f1f8613a..a04af300 100644 --- a/templates/standard/robot.yaml +++ b/templates/standard/robot.yaml @@ -19,4 +19,4 @@ PATH: PYTHONPATH: - . ignoreFiles: - - .gitignore + - .gitignore From 6ea1167f9b41d7f7b5e044d9015bce4e586456fb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 22 Dec 2020 09:29:28 +0200 Subject: [PATCH 030/516] RCC-125: adding staging support (v7.1.2) - new flag --stage with folder where things are stored - currently this flag only applies in "rcc env" part of the world - also toggled final conda.yaml visible in logs - with new flag, meta files are not saved and folder is always recreated --- cmd/env.go | 2 ++ cmd/root.go | 1 + common/variables.go | 9 +++++++++ common/version.go | 2 +- conda/robocorp.go | 3 +++ conda/workflows.go | 33 +++++++++++++++++++++++---------- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 65d84d05..fa65a0a6 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -14,4 +15,5 @@ used in task context locally.`, func init() { rootCmd.AddCommand(envCmd) + envCmd.PersistentFlags().StringVar(&common.StageFolder, "stage", "", "internal, DO NOT USE (unless you know what you are doing)") } diff --git a/cmd/root.go b/cmd/root.go index fb5379af..433dd568 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -97,6 +97,7 @@ func initConfig() { } common.UnifyVerbosityFlags() + common.UnifyStageHandling() pretty.Setup() common.Trace("CLI command was: %#v", os.Args) diff --git a/common/variables.go b/common/variables.go index 5f7bd8c6..7ac4dac3 100644 --- a/common/variables.go +++ b/common/variables.go @@ -11,6 +11,8 @@ var ( TraceFlag bool NoCache bool Liveonly bool + Stageonly bool + StageFolder string ControllerType string ) @@ -28,6 +30,13 @@ func UnifyVerbosityFlags() { } } +func UnifyStageHandling() { + if len(StageFolder) > 0 { + Liveonly = true + Stageonly = true + } +} + func ForceDebug() { Silent = false DebugFlag = true diff --git a/common/version.go b/common/version.go index 50b568df..6c07115b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.1` + Version = `v7.1.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index ec9c4d1a..598c9482 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -266,6 +266,9 @@ func TemplateFrom(hash string) string { } func LiveFrom(hash string) string { + if common.Stageonly { + return common.StageFolder + } return ExpandPath(filepath.Join(LiveLocation(), hash)) } diff --git a/conda/workflows.go b/conda/workflows.go index fe316cdc..6ea5f4fa 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -36,6 +36,9 @@ func metaLoad(location string) (string, error) { } func metaSave(location, data string) error { + if common.Stageonly { + return nil + } return ioutil.WriteFile(metafile(location), []byte(data), 0644) } @@ -60,6 +63,9 @@ func IsPristine(folder string) bool { } func reuseExistingLive(key string) bool { + if common.Stageonly { + return false + } candidate := LiveFrom(key) if IsPristine(candidate) { touchMetafile(candidate) @@ -143,7 +149,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh command = []string{CondaExecutable(), "env", "create", "-f", condaYaml, "-p", targetFolder} } if HasMicroMamba() { - command = []string{BinMicromamba(), "create", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "-q", "-y", "-f", condaYaml, "-p", targetFolder} + if common.DebugFlag { + command = []string{BinMicromamba(), "create", "-y", "-f", condaYaml, "-p", targetFolder} + } } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") @@ -220,7 +229,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. if err != nil { return "", "", nil, err } - common.Trace("FINAL union conda environment descriptior:\n---\n%v---", yaml) + common.Log("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash := shortDigest(yaml) if !save { return hash, yaml, right, nil @@ -301,21 +310,25 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { xviper.Set("stats.env.hit", hits) return liveFolder, nil } - common.Log("#### Progress: 1/4 [try clone existing same template to live, key: %v]", key) - if CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) { - dirty += 1 - xviper.Set("stats.env.dirty", dirty) - return liveFolder, nil + if common.Stageonly { + common.Log("#### Progress: 1/4 [skipped -- stage only]") + } else { + common.Log("#### Progress: 1/4 [try clone existing same template to live, key: %v]", key) + if CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) { + dirty += 1 + xviper.Set("stats.env.dirty", dirty) + return liveFolder, nil + } } common.Log("#### Progress: 2/4 [try create new environment from scratch]") if newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { misses += 1 xviper.Set("stats.env.miss", misses) - if !common.Liveonly { + if common.Liveonly { + common.Log("#### Progress: 3/4 [skipped -- live only]") + } else { common.Log("#### Progress: 3/4 [backup new environment as template]") CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) - } else { - common.Log("#### Progress: 3/4 [skipped]") } return liveFolder, nil } From c441d040f4a64dee61dd9ca84be4739b67590c96 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 22 Dec 2020 13:37:24 +0200 Subject: [PATCH 031/516] RCC-125: adding staging support (v7.1.3) - added --strict-channel-priority flag to micromamba invocation --- common/version.go | 2 +- conda/workflows.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 6c07115b..97c43d71 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.2` + Version = `v7.1.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index 6ea5f4fa..f3606b4a 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -149,9 +149,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh command = []string{CondaExecutable(), "env", "create", "-f", condaYaml, "-p", targetFolder} } if HasMicroMamba() { - command = []string{BinMicromamba(), "create", "-q", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{BinMicromamba(), "create", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-y", "-f", condaYaml, "-p", targetFolder} } } observer := make(InstallObserver) From 6570fffc12f28a0cc8f4c5e657b2859e008a05b3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Jan 2021 11:13:15 +0200 Subject: [PATCH 032/516] RCC-128: fixing background metrics sending (v7.1.4) - bug fix for background metrics not send when application ends too fast - now all telemetry sending happens in background and synchronized at the end - added this new changelog.md file --- cloud/metrics.go | 21 ++++++++++++++++----- cmd/metric.go | 2 +- cmd/rcc/main.go | 5 ++++- common/version.go | 2 +- docs/changelog.md | 12 ++++++++++++ 5 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 docs/changelog.md diff --git a/cloud/metrics.go b/cloud/metrics.go index 18a41c57..ffcbc1eb 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -3,18 +3,26 @@ package cloud import ( "fmt" "net/url" + "sync" "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/xviper" ) +var ( + telemetryBarrier = sync.WaitGroup{} +) + const ( trackingUrl = `/metric-v1/%v/%v/%v/%v/%v` metricsHost = `https://telemetry.robocorp.com` ) func sendMetric(kind, name, value string) { + defer func() { + telemetryBarrier.Done() + }() client, err := NewClient(metricsHost) if err != nil { common.Debug("ERROR: %v", err) @@ -26,13 +34,16 @@ func sendMetric(kind, name, value string) { client.Put(client.NewRequest(url)) } -func SendMetric(kind, name, value string) { - common.Debug("DEBUG: SendMetric kind:%v name:%v value:%v send:%v", kind, name, value, xviper.CanTrack()) +func BackgroundMetric(kind, name, value string) { + common.Debug("DEBUG: BackgroundMetric kind:%v name:%v value:%v send:%v", kind, name, value, xviper.CanTrack()) if xviper.CanTrack() { - sendMetric(kind, name, value) + telemetryBarrier.Add(1) + go sendMetric(kind, name, value) } } -func BackgroundMetric(kind, name, value string) { - go SendMetric(kind, name, value) +func WaitTelemetry() { + common.Debug("DEBUG: wait telemetry to complete") + telemetryBarrier.Wait() + common.Debug("DEBUG: telemetry sending completed") } diff --git a/cmd/metric.go b/cmd/metric.go index e071fd3a..475a7098 100644 --- a/cmd/metric.go +++ b/cmd/metric.go @@ -26,7 +26,7 @@ var metricCmd = &cobra.Command{ if !xviper.CanTrack() { pretty.Exit(1, "Tracking is disabled. Quitting.") } - cloud.SendMetric(metricType, metricName, metricValue) + cloud.BackgroundMetric(metricType, metricName, metricValue) pretty.Exit(0, "OK") }, } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 5f38f1e4..2573fbf7 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -14,11 +14,14 @@ func ExitProtection() { exit, ok := status.(common.ExitCode) if ok { exit.ShowMessage() + cloud.WaitTelemetry() os.Exit(exit.Code) } - cloud.SendMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) + cloud.WaitTelemetry() panic(status) } + cloud.WaitTelemetry() } func main() { diff --git a/common/version.go b/common/version.go index 97c43d71..d239657a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.3` + Version = `v7.1.4` ) diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..30dc3589 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,12 @@ +# rcc change log + +## v7.1.4 + +- bug fix for background metrics not send when application ends too fast +- now all telemetry sending happens in background and synchronized at the end +- added this new changelog.md file + +## Older versions + +Versions 7.1.3 and older do not have change log entries. This changelog.md +file was started at 4.1.2021. From d0f210fcbad318cfcd59419f2f9990108b179e05 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Jan 2021 12:31:43 +0200 Subject: [PATCH 033/516] RCC-129: make changelog command available (v7.1.5) --- Rakefile | 2 +- cmd/changelog.go | 27 +++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 cmd/changelog.go diff --git a/Rakefile b/Rakefile index 4596c6f8..a3ef3579 100644 --- a/Rakefile +++ b/Rakefile @@ -30,7 +30,7 @@ task :assets do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.zip assets/man/*" + sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.zip assets/man/* docs/changelog.md" end task :support do diff --git a/cmd/changelog.go b/cmd/changelog.go new file mode 100644 index 00000000..6eb1d23d --- /dev/null +++ b/cmd/changelog.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var changelogCmd = &cobra.Command{ + Use: "changelog", + Short: "Show the rcc changelog.", + Long: "Show the rcc changelog.", + Aliases: []string{"changes"}, + Run: func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset("docs/changelog.md") + if err != nil { + pretty.Exit(1, "Cannot show changelog.md, reason: %v", err) + } + common.Stdout("\n%s\n", content) + }, +} + +func init() { + manCmd.AddCommand(changelogCmd) +} diff --git a/common/version.go b/common/version.go index d239657a..70207bd6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.4` + Version = `v7.1.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 30dc3589..45be3ac6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v7.1.5 + +- now command `rcc man changelog` shows changelog.md from build moment + ## v7.1.4 - bug fix for background metrics not send when application ends too fast From 26062b896aebe2f4d2a5272f8e600509fe6f237d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Jan 2021 14:19:15 +0200 Subject: [PATCH 034/516] RCC-124: micromamba download support preparation (v8.0.0) - BREAKING CHANGES - removed miniconda3 download and installing - removed all conda commands (check, download, and install) - environment variables `CONDA_EXE` and `CONDA_PYTHON_EXE` are not available anymore (since we don't have conda installation anymore) - adding micromamba download, installation, and usage functionality - dropping 32-bit support from windows and linux, this is breaking change, so that is why version series goes up to v8 --- Rakefile | 18 +--- cmd/assistantRun.go | 4 +- cmd/check.go | 45 ---------- cmd/conda.go | 15 ---- cmd/condadownload.go | 25 ------ cmd/envNew.go | 4 +- cmd/install.go | 26 ------ cmd/run.go | 4 +- cmd/shell.go | 4 +- cmd/testrun.go | 4 +- cmd/variables.go | 4 +- common/version.go | 2 +- conda/cleanup.go | 10 +-- conda/download.go | 9 +- conda/environment_test.go | 8 +- conda/installing.go | 29 +++---- conda/platform_darwin_amd64.go | 20 +---- conda/platform_linux.go | 73 ---------------- conda/platform_linux_386.go | 14 ---- conda/platform_linux_amd64.go | 59 ++++++++++++- conda/platform_windows.go | 143 -------------------------------- conda/platform_windows_386.go | 18 ---- conda/platform_windows_amd64.go | 128 ++++++++++++++++++++++++++-- conda/robocorp.go | 31 ++----- conda/workflows.go | 20 ++--- docs/changelog.md | 15 +++- robot/config.go | 2 - robot/robot.go | 2 - robot_tests/conda.yaml | 4 + robot_tests/exitcodes.robot | 5 -- robot_tests/fullrun.robot | 6 +- robot_tests/resources.robot | 15 ++-- 32 files changed, 254 insertions(+), 512 deletions(-) delete mode 100644 cmd/check.go delete mode 100644 cmd/conda.go delete mode 100644 cmd/condadownload.go delete mode 100644 cmd/install.go delete mode 100644 conda/platform_linux.go delete mode 100644 conda/platform_linux_386.go delete mode 100644 conda/platform_windows.go delete mode 100644 conda/platform_windows_386.go create mode 100644 robot_tests/conda.yaml diff --git a/Rakefile b/Rakefile index a3ef3579..efc53b93 100644 --- a/Rakefile +++ b/Rakefile @@ -34,7 +34,7 @@ task :assets do end task :support do - sh 'mkdir -p tmp build/linux64 build/linux32 build/macos64 build/windows64 build/windows32' + sh 'mkdir -p tmp build/linux64 build/macos64 build/windows64' end desc 'Run tests.' @@ -50,13 +50,6 @@ task :linux64 => [:what, :test] do sh "sha256sum build/linux64/* || true" end -task :linux32 => [:what, :test] do - ENV['GOOS'] = 'linux' - ENV['GOARCH'] = '386' - sh "go build -ldflags '-s' -o build/linux32/ ./cmd/..." - sh "sha256sum build/linux32/* || true" -end - task :macos64 => [:support] do ENV['GOOS'] = 'darwin' ENV['GOARCH'] = 'amd64' @@ -71,13 +64,6 @@ task :windows64 => [:support] do sh "sha256sum build/windows64/* || true" end -task :windows32 => [:support] do - ENV['GOOS'] = 'windows' - ENV['GOARCH'] = '386' - sh "go build -ldflags '-s' -o build/windows32/ ./cmd/..." - sh "sha256sum build/windows32/* || true" -end - desc 'Setup build environment' task :robotsetup do sh "#{PYTHON} -m pip install --upgrade -r robot_requirements.txt" @@ -95,7 +81,7 @@ task :robot => :local do end desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :linux64, :linux32, :macos64, :windows64, :windows32] do +task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do sh 'ls -l $(find build -type f)' end diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index af7c902f..712f5b21 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -32,9 +32,9 @@ var assistantRunCmd = &cobra.Command{ } now := time.Now() marker := now.Unix() - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(2, "Could not get miniconda installed.") + pretty.Exit(2, "Could not get micromamba installed.") } defer xviper.RunMinutes().Done() account := operations.AccountByName(AccountName()) diff --git a/cmd/check.go b/cmd/check.go deleted file mode 100644 index a6733425..00000000 --- a/cmd/check.go +++ /dev/null @@ -1,45 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var checkCmd = &cobra.Command{ - Use: "check", - Aliases: []string{"c"}, - Short: "Check if conda is installed in managed location.", - Long: `Check if conda is installed. And optionally also force download and install -conda using "rcc conda download" and "rcc conda install" commands. `, - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Conda check took").Report() - } - if conda.HasConda() { - pretty.Exit(0, "OK.") - } - common.Debug("Conda is missing ...") - if !autoInstall { - pretty.Exit(1, "Error: No conda.") - } - common.Debug("Starting conda download ...") - if !(conda.DoDownload() || conda.DoDownload() || conda.DoDownload()) { - pretty.Exit(2, "Error: Conda download failed.") - } - common.Debug("Starting conda install ...") - if !conda.DoInstall() { - pretty.Exit(3, "Error: Conda install failed.") - } - common.Debug("Conda install completed ...") - pretty.Ok() - }, -} - -func init() { - condaCmd.AddCommand(checkCmd) - - checkCmd.Flags().BoolVarP(&autoInstall, "install", "i", false, "If conda is missing, download and install it automatically.") -} diff --git a/cmd/conda.go b/cmd/conda.go deleted file mode 100644 index b1441e63..00000000 --- a/cmd/conda.go +++ /dev/null @@ -1,15 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var condaCmd = &cobra.Command{ - Use: "conda", - Short: "Group of commands related to `conda installation`.", - Long: `Conda specific funtionality captured in this set of subcommands.`, -} - -func init() { - rootCmd.AddCommand(condaCmd) -} diff --git a/cmd/condadownload.go b/cmd/condadownload.go deleted file mode 100644 index cdd40cb8..00000000 --- a/cmd/condadownload.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var condaDownloadCmd = &cobra.Command{ - Use: "download", - Aliases: []string{"dl", "d"}, - Short: "Download the miniconda3 installer.", - Long: `Downloads the miniconda3 installer for this platform.`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - if !(conda.DoDownload() || conda.DoDownload() || conda.DoDownload()) { - pretty.Exit(1, "Download failed.") - } - }, -} - -func init() { - condaCmd.AddCommand(condaDownloadCmd) -} diff --git a/cmd/envNew.go b/cmd/envNew.go index 3eada719..21013124 100644 --- a/cmd/envNew.go +++ b/cmd/envNew.go @@ -19,9 +19,9 @@ end result will be a composite environment.`, if common.DebugFlag { defer common.Stopwatch("New environment creation lasted").Report() } - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(2, "Could not get miniconda installed.") + pretty.Exit(2, "Could not get micromamba installed.") } label, err := conda.NewEnvironment(forceFlag, args...) if err != nil { diff --git a/cmd/install.go b/cmd/install.go deleted file mode 100644 index 334868ec..00000000 --- a/cmd/install.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var installCmd = &cobra.Command{ - Use: "install", - Aliases: []string{"i"}, - Short: "Install miniconda into the managed location.", - Long: `Install miniconda into the rcc managed location. Before executing this command, -you must successfully run the "download" command and verify that the miniconda SHA256 -matches the one on the conda site.`, - Run: func(cmd *cobra.Command, args []string) { - if !conda.DoInstall() { - pretty.Exit(1, "Error: Install failed. See above.") - } - }, -} - -func init() { - condaCmd.AddCommand(installCmd) -} diff --git a/cmd/run.go b/cmd/run.go index 572724ff..9df0c692 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -27,9 +27,9 @@ in your own machine.`, if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() } - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(2, "Could not get miniconda installed.") + pretty.Exit(2, "Could not get micromamba installed.") } defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) diff --git a/cmd/shell.go b/cmd/shell.go index 362849a6..d813902b 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -21,9 +21,9 @@ command within that environment.`, if common.DebugFlag { defer common.Stopwatch("rcc shell lasted").Report() } - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(2, "Could not get miniconda installed.") + pretty.Exit(2, "Could not get micromamba installed.") } simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) if simple { diff --git a/cmd/testrun.go b/cmd/testrun.go index 3a17a8ba..35859b61 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -27,9 +27,9 @@ var testrunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(4, "Could not get miniconda installed.") + pretty.Exit(4, "Could not get micromamba installed.") } defer xviper.RunMinutes().Done() now := time.Now() diff --git a/cmd/variables.go b/cmd/variables.go index 7839effb..ab9c03f7 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -129,9 +129,9 @@ var variablesCmd = &cobra.Command{ common.Silent = silent }() - ok := conda.MustConda() + ok := conda.MustMicromamba() if !ok { - pretty.Exit(2, "Could not get miniconda installed.") + pretty.Exit(2, "Could not get micromamba installed.") } err := exportEnvironment(args, robotFile, runTask, environmentFile, workspaceId, validityTime, jsonFlag) if err != nil { diff --git a/common/version.go b/common/version.go index 70207bd6..f958e2a9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v7.1.5` + Version = `v8.0.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 6a1be7cd..d95e063a 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -41,7 +41,6 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", TemplateLocation()) common.Log("- %v", LiveLocation()) common.Log("- %v", PipCache()) - common.Log("- %v", CondaPackages()) common.Log("- %v", MambaPackages()) return nil } @@ -60,11 +59,6 @@ func spotlessCleanup(dryrun bool) error { return err } common.Debug("Removed directory %v.", PipCache()) - err = os.RemoveAll(CondaPackages()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", CondaPackages()) err = os.RemoveAll(MambaPackages()) if err != nil { return err @@ -74,10 +68,10 @@ func spotlessCleanup(dryrun bool) error { } func Cleanup(daylimit int, dryrun, orphans, all bool) error { - lockfile := MinicondaLock() + lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { - common.Log("Could not get lock on miniconda. Quitting!") + common.Log("Could not get lock on live environment. Quitting!") return err } defer locker.Release() diff --git a/conda/download.go b/conda/download.go index e6fe272d..c884036b 100644 --- a/conda/download.go +++ b/conda/download.go @@ -5,19 +5,22 @@ import ( "io" "net/http" "os" + "path/filepath" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" ) -func DownloadConda() error { - url := DownloadLink() - filename := DownloadTarget() +func DownloadMicromamba() error { + url := MicromambaLink() + filename := BinMicromamba() response, err := http.Get(url) if err != nil { return err } defer response.Body.Close() + pathlib.EnsureDirectory(filepath.Dir(BinMicromamba())) out, err := os.Create(filename) if err != nil { return err diff --git a/conda/environment_test.go b/conda/environment_test.go index e8123eae..26204dc0 100644 --- a/conda/environment_test.go +++ b/conda/environment_test.go @@ -10,11 +10,5 @@ import ( func TestHasDownloadLinkAvailable(t *testing.T) { must_be, _ := hamlet.Specifications(t) - must_be.True(len(conda.DownloadLink()) > 10) -} - -func TestCanCreateDownloadTarget(t *testing.T) { - must_be, _ := hamlet.Specifications(t) - - must_be.True(len(conda.DownloadTarget()) > 10) + must_be.True(len(conda.MicromambaLink()) > 10) } diff --git a/conda/installing.go b/conda/installing.go index a01b19cd..0a1ce03a 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -1,12 +1,14 @@ package conda import ( + "os" + + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/shell" ) -func MustConda() bool { - return HasConda() || (ValidateLocations() && (DoDownload() || DoDownload() || DoDownload()) && DoInstall()) +func MustMicromamba() bool { + return HasMicroMamba() || ((DoDownload() || DoDownload() || DoDownload()) && DoInstall()) } func DoDownload() bool { @@ -14,16 +16,16 @@ func DoDownload() bool { defer common.Stopwatch("Download done in").Report() } - common.Log("Downloading Miniconda, this may take awhile ...") + common.Log("Downloading micromamba, this may take awhile ...") - err := DownloadConda() + err := DownloadMicromamba() if err != nil { common.Error("Download", err) + os.Remove(BinMicromamba()) return false - } else { - common.Log("Verify checksum from https://docs.conda.io/en/latest/miniconda.html") - return true } + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.download", common.Version) + return true } func DoInstall() bool { @@ -31,18 +33,13 @@ func DoInstall() bool { defer common.Stopwatch("Installation done in").Report() } - if !ValidateLocations() { - return false - } - - common.Log("Installing Miniconda, this may take awhile ...") + common.Log("Making micromamba executable ...") - install := InstallCommand() - common.Debug("Running: %v", install) - _, err := shell.New(CondaEnvironment(), ".", install...).Transparent() + err := os.Chmod(BinMicromamba(), 0o755) if err != nil { common.Error("Install", err) return false } + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.install", common.Version) return true } diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 6c053f41..dd33f04f 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -36,28 +36,12 @@ func BinMicromamba() string { return ExpandPath(filepath.Join(BinLocation(), "micromamba")) } -func BinConda() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "conda")) -} - -func BinPython() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "python")) -} - func CondaPaths(prefix string) []string { return []string{prefix + binSuffix} } -func DownloadLink() string { - return "https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh" -} - -func DownloadTarget() string { - return filepath.Join(os.TempDir(), "miniconda3.sh") -} - -func InstallCommand() []string { - return []string{"bash", DownloadTarget(), "-u", "-b", "-p", MinicondaLocation()} +func MicromambaLink() string { + return "https://downloads.robocorp.com/micromamba/stable/macos64/micromamba" } func IsPosix() bool { diff --git a/conda/platform_linux.go b/conda/platform_linux.go deleted file mode 100644 index c1835483..00000000 --- a/conda/platform_linux.go +++ /dev/null @@ -1,73 +0,0 @@ -package conda - -import ( - "fmt" - "os" - "path/filepath" -) - -const ( - Newline = "\n" - defaultRobocorpLocation = "$HOME/.robocorp" - binSuffix = "/bin" -) - -var ( - FileExtensions = []string{""} - Shell = []string{"bash", "--noprofile", "--norc", "-i"} -) - -func ExpandPath(entry string) string { - intermediate := os.ExpandEnv(entry) - result, err := filepath.Abs(intermediate) - if err != nil { - return intermediate - } - return result -} - -func CondaEnvironment() []string { - env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) - return env -} - -func BinMicromamba() string { - return ExpandPath(filepath.Join(BinLocation(), "micromamba")) -} - -func BinConda() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "conda")) -} - -func BinPython() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "bin", "python")) -} - -func CondaPaths(prefix string) []string { - return []string{ExpandPath(prefix + binSuffix)} -} - -func InstallCommand() []string { - return []string{"bash", DownloadTarget(), "-u", "-b", "-p", MinicondaLocation()} -} - -func IsPosix() bool { - return true -} - -func IsWindows() bool { - return false -} - -func HasLongPathSupport() bool { - return true -} - -func ValidateLocations() bool { - return true -} - -func EnforceLongpathSupport() error { - return nil -} diff --git a/conda/platform_linux_386.go b/conda/platform_linux_386.go deleted file mode 100644 index f8159f92..00000000 --- a/conda/platform_linux_386.go +++ /dev/null @@ -1,14 +0,0 @@ -package conda - -import ( - "os" - "path/filepath" -) - -func DownloadLink() string { - return "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86.sh" -} - -func DownloadTarget() string { - return filepath.Join(os.TempDir(), "miniconda3.sh") -} diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index aeedc371..b317093a 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -1,14 +1,65 @@ package conda import ( + "fmt" "os" "path/filepath" ) -func DownloadLink() string { - return "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" +const ( + Newline = "\n" + defaultRobocorpLocation = "$HOME/.robocorp" + binSuffix = "/bin" +) + +var ( + FileExtensions = []string{""} + Shell = []string{"bash", "--noprofile", "--norc", "-i"} +) + +func MicromambaLink() string { + return "https://downloads.robocorp.com/micromamba/stable/linux64/micromamba" +} + +func ExpandPath(entry string) string { + intermediate := os.ExpandEnv(entry) + result, err := filepath.Abs(intermediate) + if err != nil { + return intermediate + } + return result +} + +func CondaEnvironment() []string { + env := os.Environ() + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + return env +} + +func BinMicromamba() string { + return ExpandPath(filepath.Join(BinLocation(), "micromamba")) +} + +func CondaPaths(prefix string) []string { + return []string{ExpandPath(prefix + binSuffix)} +} + +func IsPosix() bool { + return true +} + +func IsWindows() bool { + return false +} + +func HasLongPathSupport() bool { + return true +} + +func ValidateLocations() bool { + return true } -func DownloadTarget() string { - return filepath.Join(os.TempDir(), "miniconda3.sh") +func EnforceLongpathSupport() error { + return nil } diff --git a/conda/platform_windows.go b/conda/platform_windows.go deleted file mode 100644 index 36e12a7c..00000000 --- a/conda/platform_windows.go +++ /dev/null @@ -1,143 +0,0 @@ -package conda - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - - "golang.org/x/sys/windows/registry" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/shell" -) - -const ( - Newline = "\r\n" - defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" - librarySuffix = "\\Library" - scriptSuffix = "\\Scripts" - usrSuffix = "\\bin" - binSuffix = "\\bin" -) - -var ( - Shell = []string{"cmd.exe", "/K"} - variablePattern = regexp.MustCompile("%[a-zA-Z]+%") - FileExtensions = []string{".exe", ".com", ".bat", ".cmd", ""} -) - -func fromEnvironment(form string) string { - replacement, ok := os.LookupEnv(form[1 : len(form)-1]) - if ok { - return replacement - } - replacement, ok = os.LookupEnv(form) - if ok { - return replacement - } - return form -} - -func ExpandPath(entry string) string { - intermediate := os.ExpandEnv(entry) - intermediate = variablePattern.ReplaceAllStringFunc(intermediate, fromEnvironment) - result, err := filepath.Abs(intermediate) - if err != nil { - return intermediate - } - return result -} - -func ensureHardlinkEnvironmment() (string, error) { - return "", fmt.Errorf("Not implemented yet!") -} - -func CondaEnvironment() []string { - env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) - /* - path, err := ensureHardlinkEnvironmment() - if err != nil { - return nil - } - env = append(env, fmt.Sprintf("TEMP=%s", path)) - env = append(env, fmt.Sprintf("TMP=%s", path)) - */ - return env -} - -func BinMicromamba() string { - return ExpandPath(filepath.Join(BinLocation(), "micromamba.exe")) -} - -func BinConda() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "Scripts", "conda.exe")) -} - -func BinPython() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "python.exe")) -} - -func CondaPaths(prefix string) []string { - return []string{ - prefix, - prefix + librarySuffix + mingwSuffix + binSuffix, - prefix + librarySuffix + usrSuffix + binSuffix, - prefix + librarySuffix + binSuffix, - prefix + scriptSuffix, - prefix + binSuffix, - } -} - -func InstallCommand() []string { - return []string{DownloadTarget(), "/InstallationType=JustMe", "/NoRegisty=1", "/S", "/D=" + MinicondaLocation()} -} - -func IsPosix() bool { - return false -} - -func IsWindows() bool { - return true -} - -func ValidateLocations() bool { - checked := map[string]string{ - "Environment variable 'TMP'": os.Getenv("TMP"), - "Environment variable 'TEMP'": os.Getenv("TEMP"), - "Environment variable 'ROBOCORP_HOME'": os.Getenv("ROBOCORP_HOME"), - "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), - } - return validateLocations(checked) -} - -func HasLongPathSupport() bool { - baseline := []string{RobocorpHome(), "stump"} - stumpath := filepath.Join(baseline...) - defer os.RemoveAll(stumpath) - - for count := 0; count < 24; count++ { - baseline = append(baseline, fmt.Sprintf("verylongpath%d", count+1)) - } - fullpath := filepath.Join(baseline...) - - code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).Transparent() - common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) - if err != nil { - common.Log("%sWARNING! Long path support failed. Reason: %v.%s", pretty.Red, err, pretty.Reset) - common.Log("%sWARNING! See %v for more details.%s", pretty.Red, longPathSupportArticle, pretty.Reset) - return false - } - return true -} - -func EnforceLongpathSupport() error { - key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\FileSystem`, registry.SET_VALUE) - if err != nil { - return err - } - defer key.Close() - return key.SetDWordValue("LongPathsEnabled", 1) -} diff --git a/conda/platform_windows_386.go b/conda/platform_windows_386.go deleted file mode 100644 index bd1440b6..00000000 --- a/conda/platform_windows_386.go +++ /dev/null @@ -1,18 +0,0 @@ -package conda - -import ( - "os" - "path/filepath" -) - -const ( - mingwSuffix = "\\mingw-w32" -) - -func DownloadLink() string { - return "https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86.exe" -} - -func DownloadTarget() string { - return filepath.Join(os.TempDir(), "miniconda3.exe") -} diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 4a2d6f93..d6f8f2c7 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -1,18 +1,136 @@ package conda import ( + "fmt" "os" "path/filepath" + "regexp" + + "golang.org/x/sys/windows/registry" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/shell" ) const ( - mingwSuffix = "\\mingw-w64" + mingwSuffix = "\\mingw-w64" + Newline = "\r\n" + defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" + librarySuffix = "\\Library" + scriptSuffix = "\\Scripts" + usrSuffix = "\\bin" + binSuffix = "\\bin" ) -func DownloadLink() string { - return "https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe" +func MicromambaLink() string { + return "https://downloads.robocorp.com/micromamba/stable/windows64/micromamba.exe" +} + +var ( + Shell = []string{"cmd.exe", "/K"} + variablePattern = regexp.MustCompile("%[a-zA-Z]+%") + FileExtensions = []string{".exe", ".com", ".bat", ".cmd", ""} +) + +func fromEnvironment(form string) string { + replacement, ok := os.LookupEnv(form[1 : len(form)-1]) + if ok { + return replacement + } + replacement, ok = os.LookupEnv(form) + if ok { + return replacement + } + return form +} + +func ExpandPath(entry string) string { + intermediate := os.ExpandEnv(entry) + intermediate = variablePattern.ReplaceAllStringFunc(intermediate, fromEnvironment) + result, err := filepath.Abs(intermediate) + if err != nil { + return intermediate + } + return result +} + +func ensureHardlinkEnvironmment() (string, error) { + return "", fmt.Errorf("Not implemented yet!") +} + +func CondaEnvironment() []string { + env := os.Environ() + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + /* + path, err := ensureHardlinkEnvironmment() + if err != nil { + return nil + } + env = append(env, fmt.Sprintf("TEMP=%s", path)) + env = append(env, fmt.Sprintf("TMP=%s", path)) + */ + return env +} + +func BinMicromamba() string { + return ExpandPath(filepath.Join(BinLocation(), "micromamba.exe")) +} + +func CondaPaths(prefix string) []string { + return []string{ + prefix, + prefix + librarySuffix + mingwSuffix + binSuffix, + prefix + librarySuffix + usrSuffix + binSuffix, + prefix + librarySuffix + binSuffix, + prefix + scriptSuffix, + prefix + binSuffix, + } +} + +func IsPosix() bool { + return false +} + +func IsWindows() bool { + return true +} + +func ValidateLocations() bool { + checked := map[string]string{ + "Environment variable 'TMP'": os.Getenv("TMP"), + "Environment variable 'TEMP'": os.Getenv("TEMP"), + "Environment variable 'ROBOCORP_HOME'": os.Getenv("ROBOCORP_HOME"), + "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), + } + return validateLocations(checked) +} + +func HasLongPathSupport() bool { + baseline := []string{RobocorpHome(), "stump"} + stumpath := filepath.Join(baseline...) + defer os.RemoveAll(stumpath) + + for count := 0; count < 24; count++ { + baseline = append(baseline, fmt.Sprintf("verylongpath%d", count+1)) + } + fullpath := filepath.Join(baseline...) + + code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).Transparent() + common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) + if err != nil { + common.Log("%sWARNING! Long path support failed. Reason: %v.%s", pretty.Red, err, pretty.Reset) + common.Log("%sWARNING! See %v for more details.%s", pretty.Red, longPathSupportArticle, pretty.Reset) + return false + } + return true } -func DownloadTarget() string { - return filepath.Join(os.TempDir(), "miniconda3.exe") +func EnforceLongpathSupport() error { + key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\FileSystem`, registry.SET_VALUE) + if err != nil { + return err + } + defer key.Close() + return key.SetDWordValue("LongPathsEnabled", 1) } diff --git a/conda/robocorp.go b/conda/robocorp.go index 598c9482..662e99e3 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -152,10 +152,8 @@ func EnvironmentExtensionFor(location string) []string { } return append(environment, "CONDA_DEFAULT_ENV=rcc", - "CONDA_EXE="+BinConda(), "CONDA_PREFIX="+location, "CONDA_PROMPT_MODIFIER=(rcc)", - "CONDA_PYTHON_EXE="+BinPython(), "CONDA_SHLVL=1", "PYTHONHOME=", "PYTHONSTARTUP=", @@ -171,32 +169,12 @@ func EnvironmentFor(location string) []string { return append(os.Environ(), EnvironmentExtensionFor(location)...) } -func CondaExecutable() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "condabin", "conda")) -} - func MambaPackages() string { return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) } -func CondaPackages() string { - return ExpandPath(filepath.Join(MinicondaLocation(), "pkgs")) -} - -func CondaCache() string { - return ExpandPath(filepath.Join(CondaPackages(), "cache")) -} - -func HasConda() bool { - if HasMicroMamba() { - return true - } - location := ExpandPath(filepath.Join(MinicondaLocation(), "condabin")) - stat, err := os.Stat(location) - if err == nil && stat.IsDir() { - return true - } - return false +func MambaCache() string { + return ExpandPath(filepath.Join(MambaPackages(), "cache")) } func HasMicroMamba() bool { @@ -223,11 +201,12 @@ func TemplateLocation() string { return filepath.Join(RobocorpHome(), "base") } -func MinicondaLock() string { - return fmt.Sprintf("%s.lck", MinicondaLocation()) +func RobocorpLock() string { + return fmt.Sprintf("%s.lck", LiveLocation()) } func MinicondaLocation() string { + // Legacy function, but must remain until cleanup is done return filepath.Join(RobocorpHome(), "miniconda3") } diff --git a/conda/workflows.go b/conda/workflows.go index f3606b4a..f2a4b021 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -109,7 +109,7 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) removeClone(targetFolder) - location := filepath.Join(MinicondaLocation(), "pkgs") + location := filepath.Join(RobocorpHome(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) common.Log("%sWARNING! To fix it, try to remove directory: %v%s", pretty.Red, location, pretty.Reset) return true @@ -140,19 +140,13 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh when = when.Add(-20 * 24 * time.Hour) } if force || !freshInstall { - common.Log("rcc touching conda cache. (Stamp: %v)", when) - SilentTouch(CondaCache(), when) + common.Log("rcc touching mamba cache. (Stamp: %v)", when) + SilentTouch(MambaCache(), when) } common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) - command := []string{CondaExecutable(), "env", "create", "-q", "-f", condaYaml, "-p", targetFolder} + command := []string{BinMicromamba(), "create", "--strict-channel-priority", "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{CondaExecutable(), "env", "create", "-f", condaYaml, "-p", targetFolder} - } - if HasMicroMamba() { - command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-q", "-y", "-f", condaYaml, "-p", targetFolder} - if common.DebugFlag { - command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-y", "-f", condaYaml, "-p", targetFolder} - } + command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") @@ -261,10 +255,10 @@ func CalculateComboHash(configurations ...string) (string, error) { func NewEnvironment(force bool, configurations ...string) (string, error) { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) - lockfile := MinicondaLock() + lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { - common.Log("Could not get lock on miniconda. Quitting!") + common.Log("Could not get lock on live environment. Quitting!") return "", err } defer locker.Release() diff --git a/docs/changelog.md b/docs/changelog.md index 45be3ac6..1780807c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,10 +1,21 @@ # rcc change log -## v7.1.5 +## v8.0.0 (date: 5.1.2021) + +- BREAKING CHANGES +- removed miniconda3 download and installing +- removed all conda commands (check, download, and install) +- environment variables `CONDA_EXE` and `CONDA_PYTHON_EXE` are not available + anymore (since we don't have conda installation anymore) +- adding micromamba download, installation, and usage functionality +- dropping 32-bit support from windows and linux, this is breaking change, + so that is why version series goes up to v8 + +## v7.1.5 (date: 4.1.2021) - now command `rcc man changelog` shows changelog.md from build moment -## v7.1.4 +## v7.1.4 (date: 4.1.2021) - bug fix for background metrics not send when application ends too fast - now all telemetry sending happens in background and synchronized at the end diff --git a/robot/config.go b/robot/config.go index 7a9f4767..88e49a30 100644 --- a/robot/config.go +++ b/robot/config.go @@ -229,10 +229,8 @@ func (it *Activity) ExecutionEnvironment(base Robot, location string, inject []s } return append(environment, "CONDA_DEFAULT_ENV=rcc", - "CONDA_EXE="+conda.BinConda(), "CONDA_PREFIX="+location, "CONDA_PROMPT_MODIFIER=(rcc)", - "CONDA_PYTHON_EXE="+conda.BinPython(), "CONDA_SHLVL=1", "PYTHONHOME=", "PYTHONSTARTUP=", diff --git a/robot/robot.go b/robot/robot.go index 23845379..adfc1d94 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -211,10 +211,8 @@ func (it *robot) ExecutionEnvironment(taskname, location string, inject []string } return append(environment, "CONDA_DEFAULT_ENV=rcc", - "CONDA_EXE="+conda.BinConda(), "CONDA_PREFIX="+location, "CONDA_PROMPT_MODIFIER=(rcc)", - "CONDA_PYTHON_EXE="+conda.BinPython(), "CONDA_SHLVL=1", "PYTHONHOME=", "PYTHONSTARTUP=", diff --git a/robot_tests/conda.yaml b/robot_tests/conda.yaml new file mode 100644 index 00000000..3fa8c8e8 --- /dev/null +++ b/robot_tests/conda.yaml @@ -0,0 +1,4 @@ +channels: +- conda-forge +dependencies: +- python=3.7.8 diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index 9d716396..e01fca93 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -12,7 +12,6 @@ Help for rcc command 0 build/rcc -h Help for rcc assistant subcommand 0 build/rcc assistant -h --controller citests Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citests -Help for rcc conda subcommand 0 build/rcc conda -h --controller citests Help for rcc configure subcommand 0 build/rcc configure -h --controller citests Help for rcc env subcommand 0 build/rcc env -h --controller citests Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests @@ -35,10 +34,6 @@ Help for rcc cloud upload 0 build/rcc cloud upload -h --controller Help for rcc cloud userinfo 0 build/rcc cloud userinfo -h --controller citests Help for rcc cloud workspace 0 build/rcc cloud workspace -h --controller citests -Help for rcc conda check 0 build/rcc conda check -h --controller citests -Help for rcc conda download 0 build/rcc conda download -h --controller citests -Help for rcc conda install 0 build/rcc conda install -h --controller citests - Help for rcc configure credentials 0 build/rcc configure credentials -h --controller citests Help for rcc env delete 0 build/rcc env delete -h --controller citests diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index ebb9b652..22855a54 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -9,7 +9,7 @@ Using and running template example with shell file Goal Show rcc version information. Step build/rcc version --controller citests - Must Have v7. + Must Have v8. Goal Show rcc license information. Step build/rcc man license --controller citests @@ -105,10 +105,8 @@ Using and running template example with shell file Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc - Must Have CONDA_EXE= Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) - Must Have CONDA_PYTHON_EXE= Must Have CONDA_SHLVL=1 Must Have PATH= Must Have PYTHONPATH= @@ -128,10 +126,8 @@ Using and running template example with shell file Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc - Must Have CONDA_EXE= Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) - Must Have CONDA_PYTHON_EXE= Must Have CONDA_SHLVL=1 Must Have PATH= Must Have PYTHONPATH= diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 0d710b3c..fe7c990e 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -15,14 +15,13 @@ Prepare Local Create Directory tmp/robocorp Set Environment Variable ROBOCORP_HOME tmp/robocorp - Goal Verify miniconda is installed or download and install it. - Step build/rcc conda check -i - Must Have OK. - Must Exist %{ROBOCORP_HOME}/miniconda3/ - Wont Exist %{ROBOCORP_HOME}/base/ - Wont Exist %{ROBOCORP_HOME}/live/ - Wont Exist %{ROBOCORP_HOME}/wheels/ - Wont Exist %{ROBOCORP_HOME}/pipcache/ + Goal Verify micromamba is installed or download and install it. + Step build/rcc env new robot_tests/conda.yaml + Must Exist %{ROBOCORP_HOME}/bin/ + Must Exist %{ROBOCORP_HOME}/base/ + Must Exist %{ROBOCORP_HOME}/live/ + Must Exist %{ROBOCORP_HOME}/wheels/ + Must Exist %{ROBOCORP_HOME}/pipcache/ Goal [Arguments] ${anything} From 14ec7e556106ae5269453fb16259c5f56833b7bb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Jan 2021 15:39:51 +0200 Subject: [PATCH 035/516] RCC-124: micromamba download support cleanup (v8.0.1) - added separate pip install phase progress step (just visualization) - now `rcc env cleanup` has option to remove miniconda3 installation --- cmd/cleanup.go | 10 ++++++---- common/version.go | 2 +- conda/cleanup.go | 20 +++++++++++++++++--- conda/workflows.go | 15 ++++++++------- docs/changelog.md | 5 +++++ shell/task.go | 3 +++ 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 85ea2aaa..1a521a31 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -9,9 +9,10 @@ import ( ) var ( - allFlag bool - orphanFlag bool - daysOption int + allFlag bool + orphanFlag bool + minicondaFlag bool + daysOption int ) var cleanupCmd = &cobra.Command{ @@ -23,7 +24,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag) + err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag, minicondaFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -35,6 +36,7 @@ func init() { envCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") + cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Cleanup all enviroments.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") } diff --git a/common/version.go b/common/version.go index f958e2a9..9ccba54c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.0` + Version = `v8.0.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index d95e063a..d5eeb5e7 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -8,6 +8,17 @@ import ( "github.com/robocorp/rcc/pathlib" ) +func doCleanup(fullpath string, dryrun bool) error { + if !pathlib.Exists(fullpath) { + return nil + } + if dryrun { + common.Log("Would be removing: %s", fullpath) + return nil + } + return os.RemoveAll(fullpath) +} + func orphanCleanup(dryrun bool) error { orphans := OrphanList() if len(orphans) == 0 { @@ -67,7 +78,7 @@ func spotlessCleanup(dryrun bool) error { return nil } -func Cleanup(daylimit int, dryrun, orphans, all bool) error { +func Cleanup(daylimit int, dryrun, orphans, all, miniconda bool) error { lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -105,7 +116,10 @@ func Cleanup(daylimit int, dryrun, orphans, all bool) error { common.Debug("Removed environment %v.", template) } if orphans { - return orphanCleanup(dryrun) + err = orphanCleanup(dryrun) } - return nil + if miniconda && err == nil { + err = doCleanup(MinicondaLocation(), dryrun) + } + return err } diff --git a/conda/workflows.go b/conda/workflows.go index f2a4b021..8874ae74 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -163,6 +163,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if common.DebugFlag { pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", PipCache(), "--find-links", WheelCache(), "--requirement", requirementsText} } + common.Log("#### Progress: 3/5 [pip install phase]") common.Debug("=== new live --- pip install phase ===") err = LiveExecution(targetFolder, pipCommand...) if err != nil { @@ -274,9 +275,9 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { freshInstall := templates == 0 defer func() { - common.Log("#### Progress: 4/4 [Done.] [Stats: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) + common.Log("#### Progress: 5/5 [Done.] [Stats: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) }() - common.Log("#### Progress: 0/4 [try use existing live same environment?] %v", xviper.TrackingIdentity()) + common.Log("#### Progress: 0/5 [try use existing live same environment?] %v", xviper.TrackingIdentity()) xviper.Set("stats.env.request", requests) @@ -305,23 +306,23 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } if common.Stageonly { - common.Log("#### Progress: 1/4 [skipped -- stage only]") + common.Log("#### Progress: 1/5 [skipped -- stage only]") } else { - common.Log("#### Progress: 1/4 [try clone existing same template to live, key: %v]", key) + common.Log("#### Progress: 1/5 [try clone existing same template to live, key: %v]", key) if CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) { dirty += 1 xviper.Set("stats.env.dirty", dirty) return liveFolder, nil } } - common.Log("#### Progress: 2/4 [try create new environment from scratch]") + common.Log("#### Progress: 2/5 [try create new environment from scratch]") if newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { misses += 1 xviper.Set("stats.env.miss", misses) if common.Liveonly { - common.Log("#### Progress: 3/4 [skipped -- live only]") + common.Log("#### Progress: 4/5 [skipped -- live only]") } else { - common.Log("#### Progress: 3/4 [backup new environment as template]") + common.Log("#### Progress: 4/5 [backup new environment as template]") CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) } return liveFolder, nil diff --git a/docs/changelog.md b/docs/changelog.md index 1780807c..cfd8948d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v8.0.1 (date: 5.1.2021) + +- added separate pip install phase progress step (just visualization) +- now `rcc env cleanup` has option to remove miniconda3 installation + ## v8.0.0 (date: 5.1.2021) - BREAKING CHANGES diff --git a/shell/task.go b/shell/task.go index 271d5e51..ac71f243 100644 --- a/shell/task.go +++ b/shell/task.go @@ -5,6 +5,8 @@ import ( "os" "os/exec" "path/filepath" + + "github.com/robocorp/rcc/common" ) type Task struct { @@ -39,6 +41,7 @@ func (it *Task) stdout() io.Writer { } func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) { + common.Trace("Execute %q with arguments %q", it.executable, it.args) command := exec.Command(it.executable, it.args...) command.Env = it.environment command.Dir = it.directory From 10d0bae0414643eaadcddb2630644426072bd7cc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Jan 2021 15:53:09 +0200 Subject: [PATCH 036/516] RCC-124: micromamba download support cleanup (v8.0.2) - fixing failed robot tests for progress indicators (just tests) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ robot_tests/fullrun.robot | 15 ++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/common/version.go b/common/version.go index 9ccba54c..999e1a05 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.1` + Version = `v8.0.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index cfd8948d..aa9a1e27 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v8.0.2 (date: 5.1.2021) + +- fixing failed robot tests for progress indicators (just tests) + ## v8.0.1 (date: 5.1.2021) - added separate pip install phase progress step (just visualization) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 22855a54..bfd02fba 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -71,8 +71,8 @@ Using and running template example with shell file Goal Run task in place. Step build/rcc task run --controller citests -r tmp/fluffy/robot.yaml - Must Have Progress: 0/4 - Must Have Progress: 4/4 + Must Have Progress: 0/5 + Must Have Progress: 5/5 Must Have rpaframework Must Have 1 critical task, 1 passed, 0 failed Must Have OK. @@ -83,11 +83,12 @@ Using and running template example with shell file Goal Run task in clean temporary directory. Step build/rcc task testrun --controller citests -r tmp/fluffy/robot.yaml - Must Have Progress: 0/4 - Wont Have Progress: 1/4 - Wont Have Progress: 2/4 - Wont Have Progress: 3/4 - Must Have Progress: 4/4 + Must Have Progress: 0/5 + Wont Have Progress: 1/5 + Wont Have Progress: 2/5 + Wont Have Progress: 3/5 + Wont Have Progress: 4/5 + Must Have Progress: 5/5 Must Have rpaframework Must Have 1 critical task, 1 passed, 0 failed Must Have OK. From 9160dd45bdf53aeb30c7f1d9fff4e7313ea5cbf5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 7 Jan 2021 15:23:04 +0200 Subject: [PATCH 037/516] RCC-124: micromamba -- pip related warnings (v8.0.3) - adding path validation warnings, since they became problem (with pip) now that we moved to use micromamba instead of miniconda - also validation pattern update, with added "~" and "-" as valid characters - validation is now done on toplevel, so all commands could generate those warnings (but currently they don't break anything yet) --- cmd/root.go | 1 + common/version.go | 2 +- conda/platform_darwin_amd64.go | 4 ---- conda/platform_linux_amd64.go | 4 ---- conda/platform_windows_amd64.go | 10 ---------- conda/validate.go | 22 ++++++++++++++++++---- docs/changelog.md | 8 ++++++++ 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 433dd568..d21fb6e9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -102,4 +102,5 @@ func initConfig() { pretty.Setup() common.Trace("CLI command was: %#v", os.Args) common.Debug("Using config file: %v", xviper.ConfigFileUsed()) + conda.ValidateLocations() } diff --git a/common/version.go b/common/version.go index 999e1a05..d2b08d81 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.2` + Version = `v8.0.3` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index dd33f04f..3511e39d 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -56,10 +56,6 @@ func HasLongPathSupport() bool { return true } -func ValidateLocations() bool { - return true -} - func EnforceLongpathSupport() error { return nil } diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index b317093a..0a3b4df7 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -56,10 +56,6 @@ func HasLongPathSupport() bool { return true } -func ValidateLocations() bool { - return true -} - func EnforceLongpathSupport() error { return nil } diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index d6f8f2c7..73aaa7e2 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -96,16 +96,6 @@ func IsWindows() bool { return true } -func ValidateLocations() bool { - checked := map[string]string{ - "Environment variable 'TMP'": os.Getenv("TMP"), - "Environment variable 'TEMP'": os.Getenv("TEMP"), - "Environment variable 'ROBOCORP_HOME'": os.Getenv("ROBOCORP_HOME"), - "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), - } - return validateLocations(checked) -} - func HasLongPathSupport() bool { baseline := []string{RobocorpHome(), "stump"} stumpath := filepath.Join(baseline...) diff --git a/conda/validate.go b/conda/validate.go index 05506d91..a6da9c8e 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -1,6 +1,7 @@ package conda import ( + "os" "regexp" "strings" @@ -13,7 +14,7 @@ const ( ) var ( - validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\]+$") + validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") ) func validateLocations(checked map[string]string) bool { @@ -24,15 +25,28 @@ func validateLocations(checked map[string]string) bool { } if strings.ContainsAny(value, " \t") { success = false - common.Log("%sWARNING! %s contain spaces. Cannot install miniconda at %v.%s", pretty.Red, name, value, pretty.Reset) + common.Log("%sWARNING! %s contain spaces. Cannot use tooling with path %q.%s", pretty.Red, name, value, pretty.Reset) } if !validPathCharacters.MatchString(value) { success = false - common.Log("%sWARNING! %s contain illegal characters. Cannot install miniconda at %v.%s", pretty.Red, name, value, pretty.Reset) + common.Log("%sWARNING! %s contain illegal characters. Cannot use tooling with path %q.%s", pretty.Red, name, value, pretty.Reset) } } if !success { - common.Log("%sERROR! Cannot install miniconda on your system. See above.%s", pretty.Red, pretty.Reset) + common.Log("%sWARNING! Python pip might not work correctly in your system. See above.%s", pretty.Red, pretty.Reset) } return success } + +func ValidateLocations() bool { + checked := map[string]string{ + "Environment variable 'TMP'": os.Getenv("TMP"), + "Environment variable 'TEMP'": os.Getenv("TEMP"), + "Environment variable 'ROBOCORP_HOME'": os.Getenv("ROBOCORP_HOME"), + "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), + } + // 7.1.2021 -- just warnings for now -- JMP:FIXME:JMP later + validateLocations(checked) + return true + // return validateLocations(checked) +} diff --git a/docs/changelog.md b/docs/changelog.md index aa9a1e27..38c121b2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v8.0.3 (date: 7.1.2021) + +- adding path validation warnings, since they became problem (with pip) now + that we moved to use micromamba instead of miniconda +- also validation pattern update, with added "~" and "-" as valid characters +- validation is now done on toplevel, so all commands could generate + those warnings (but currently they don't break anything yet) + ## v8.0.2 (date: 5.1.2021) - fixing failed robot tests for progress indicators (just tests) From d57289921447e75f4f817ae4cbbc52dac2fb318d Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Fri, 8 Jan 2021 13:07:06 +0200 Subject: [PATCH 038/516] Robot template updates rpaframework updated to a specific 7.1.1 Added guiding comments. --- templates/extended/conda.yaml | 10 +++++++++- templates/python/conda.yaml | 10 +++++++++- templates/standard/conda.yaml | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index b8f48500..c00993f2 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -1,8 +1,16 @@ channels: + # Define conda channels here. - defaults - conda-forge + dependencies: + # Define conda packages here. + # We recommend using packages from the conda-forge channel. + # https://anaconda.org/search - python=3.7.5 + - pip=20.1 - pip: - - rpaframework==6.* + # Define pip packages here. + # https://pypi.org/ + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index b8f48500..c00993f2 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -1,8 +1,16 @@ channels: + # Define conda channels here. - defaults - conda-forge + dependencies: + # Define conda packages here. + # We recommend using packages from the conda-forge channel. + # https://anaconda.org/search - python=3.7.5 + - pip=20.1 - pip: - - rpaframework==6.* + # Define pip packages here. + # https://pypi.org/ + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index b8f48500..c00993f2 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -1,8 +1,16 @@ channels: + # Define conda channels here. - defaults - conda-forge + dependencies: + # Define conda packages here. + # We recommend using packages from the conda-forge channel. + # https://anaconda.org/search - python=3.7.5 + - pip=20.1 - pip: - - rpaframework==6.* + # Define pip packages here. + # https://pypi.org/ + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file From 5bbc309f3c0305a8bb1829a07c84aca26a6a4e61 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 8 Jan 2021 10:50:39 +0200 Subject: [PATCH 039/516] RCC-124: micromamba -- repodata ttl usage (v8.0.4) - now requires micromamba 0.7.7 at least, with version check added - micromamba now brings --repodata-ttl, which rcc currently sets for 7 days - and touching conda caches is gone because of repodata ttl - can now also cleanup micromamba binary and with --all - environment validation checks simplified (no more separate space check) --- cmd/cleanup.go | 12 +++++++----- common/version.go | 2 +- conda/cleanup.go | 14 +++++++++++++- conda/robocorp.go | 34 +++++++++++++++++++++++++++++++++- conda/validate.go | 16 +++++----------- conda/workflows.go | 12 ++---------- docs/changelog.md | 8 ++++++++ shell/task.go | 8 ++++++++ 8 files changed, 77 insertions(+), 29 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 1a521a31..8ac57619 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -9,10 +9,11 @@ import ( ) var ( - allFlag bool - orphanFlag bool - minicondaFlag bool - daysOption int + allFlag bool + orphanFlag bool + minicondaFlag bool + micromambaFlag bool + daysOption int ) var cleanupCmd = &cobra.Command{ @@ -24,7 +25,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag, minicondaFlag) + err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag, minicondaFlag, micromambaFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -37,6 +38,7 @@ func init() { cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") + cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Cleanup all enviroments.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") } diff --git a/common/version.go b/common/version.go index d2b08d81..0a8d7ac4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.3` + Version = `v8.0.4` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index d5eeb5e7..2b0835f9 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -53,6 +53,7 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", LiveLocation()) common.Log("- %v", PipCache()) common.Log("- %v", MambaPackages()) + common.Log("- %v", BinMicromamba()) return nil } err := os.RemoveAll(TemplateLocation()) @@ -75,10 +76,15 @@ func spotlessCleanup(dryrun bool) error { return err } common.Debug("Removed directory %v.", MambaPackages()) + err = os.Remove(BinMicromamba()) + if err != nil { + return err + } + common.Debug("Removed executable %v.", BinMicromamba()) return nil } -func Cleanup(daylimit int, dryrun, orphans, all, miniconda bool) error { +func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) error { lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -121,5 +127,11 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda bool) error { if miniconda && err == nil { err = doCleanup(MinicondaLocation(), dryrun) } + if micromamba && err == nil { + err = doCleanup(MambaPackages(), dryrun) + } + if micromamba && err == nil { + err = doCleanup(BinMicromamba(), dryrun) + } return err } diff --git a/conda/robocorp.go b/conda/robocorp.go index 662e99e3..4bd5a161 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -7,9 +7,12 @@ import ( "path/filepath" "regexp" "sort" + "strconv" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/shell" ) const ( @@ -177,8 +180,37 @@ func MambaCache() string { return ExpandPath(filepath.Join(MambaPackages(), "cache")) } +func asVersion(text string) (uint64, string) { + text = strings.TrimSpace(text) + parts := strings.SplitN(text, ".", 4) + steps := len(parts) + multipliers := []uint64{1000000, 1000, 1} + version := uint64(0) + for at, multiplier := range multipliers { + if steps <= at { + break + } + value, err := strconv.ParseUint(parts[at], 10, 64) + if err != nil { + break + } + version += multiplier * value + } + return version, text +} + func HasMicroMamba() bool { - return pathlib.IsFile(BinMicromamba()) + if !pathlib.IsFile(BinMicromamba()) { + return false + } + versionText, _, err := shell.New(nil, ".", BinMicromamba(), "--version").CaptureOutput() + if err != nil { + return false + } + version, versionText := asVersion(versionText) + goodEnough := version >= 7007 + common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) + return goodEnough } func RobocorpHome() string { diff --git a/conda/validate.go b/conda/validate.go index a6da9c8e..51a2ca83 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -3,7 +3,6 @@ package conda import ( "os" "regexp" - "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" @@ -23,27 +22,22 @@ func validateLocations(checked map[string]string) bool { if len(value) == 0 { continue } - if strings.ContainsAny(value, " \t") { - success = false - common.Log("%sWARNING! %s contain spaces. Cannot use tooling with path %q.%s", pretty.Red, name, value, pretty.Reset) - } if !validPathCharacters.MatchString(value) { success = false - common.Log("%sWARNING! %s contain illegal characters. Cannot use tooling with path %q.%s", pretty.Red, name, value, pretty.Reset) + common.Log("%sWARNING! %s contain illegal characters. Cannot use tooling with path %q.%s", pretty.Yellow, name, value, pretty.Reset) } } if !success { - common.Log("%sWARNING! Python pip might not work correctly in your system. See above.%s", pretty.Red, pretty.Reset) + common.Log("%sWARNING! Python pip might not work correctly in your system. See above.%s", pretty.Yellow, pretty.Reset) } return success } func ValidateLocations() bool { checked := map[string]string{ - "Environment variable 'TMP'": os.Getenv("TMP"), - "Environment variable 'TEMP'": os.Getenv("TEMP"), - "Environment variable 'ROBOCORP_HOME'": os.Getenv("ROBOCORP_HOME"), - "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), + "Environment variable 'TMP'": os.Getenv("TMP"), + "Environment variable 'TEMP'": os.Getenv("TEMP"), + "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), } // 7.1.2021 -- just warnings for now -- JMP:FIXME:JMP later validateLocations(checked) diff --git a/conda/workflows.go b/conda/workflows.go index 8874ae74..e461ca80 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -135,18 +135,10 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) - when := time.Now() - if force { - when = when.Add(-20 * 24 * time.Hour) - } - if force || !freshInstall { - common.Log("rcc touching mamba cache. (Stamp: %v)", when) - SilentTouch(MambaCache(), when) - } common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) - command := []string{BinMicromamba(), "create", "--strict-channel-priority", "-q", "-y", "-f", condaYaml, "-p", targetFolder} + command := []string{BinMicromamba(), "create", "--strict-channel-priority", "--repodata-ttl", "604800", "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{BinMicromamba(), "create", "--strict-channel-priority", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--strict-channel-priority", "--repodata-ttl", "604800", "-v", "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 38c121b2..abec35bc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v8.0.4 (date: 8.1.2021) + +- now requires micromamba 0.7.7 at least, with version check added +- micromamba now brings --repodata-ttl, which rcc currently sets for 7 days +- and touching conda caches is gone because of repodata ttl +- can now also cleanup micromamba binary and with --all +- environment validation checks simplified (no more separate space check) + ## v8.0.3 (date: 7.1.2021) - adding path validation warnings, since they became problem (with pip) now diff --git a/shell/task.go b/shell/task.go index ac71f243..ce13aa61 100644 --- a/shell/task.go +++ b/shell/task.go @@ -1,6 +1,7 @@ package shell import ( + "bytes" "io" "os" "os/exec" @@ -94,3 +95,10 @@ func (it *Task) Observed(sink io.Writer, interactive bool) (int, error) { } return it.execute(os.Stdin, stdout, stderr) } + +func (it *Task) CaptureOutput() (string, int, error) { + os.Stdin.Close() + stdout := bytes.NewBuffer(nil) + code, err := it.execute(os.Stdin, stdout, os.Stderr) + return stdout.String(), code, err +} From 750c7dfb89c9a91e3b2b863981f771c051e13255 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 8 Jan 2021 14:02:50 +0200 Subject: [PATCH 040/516] OTHER: robot tests for development process (v8.0.5) --- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot_tests/development_process.robot | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 robot_tests/development_process.robot diff --git a/common/version.go b/common/version.go index 0a8d7ac4..538d5d7d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.4` + Version = `v8.0.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index abec35bc..2b15416c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v8.0.5 (date: 8.1.2021) + +- added robot test to validate required changes, which are common/version.go + and docs/changelog.md + ## v8.0.4 (date: 8.1.2021) - now requires micromamba 0.7.7 at least, with version check added diff --git a/robot_tests/development_process.robot b/robot_tests/development_process.robot new file mode 100644 index 00000000..7e846831 --- /dev/null +++ b/robot_tests/development_process.robot @@ -0,0 +1,12 @@ +*** Settings *** +Resource resources.robot + +*** Test cases *** + +Has required changes in commit based on development process. + + Goal See git changes for required files have been changed + Step git show --stat + Must Have docs/changelog.md + Must Have common/version.go + From da51ee2ff2a0668f377df6742f58fcc377ecb935 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Fri, 8 Jan 2021 16:32:15 +0200 Subject: [PATCH 041/516] Review changes --- templates/extended/conda.yaml | 4 ++-- templates/python/conda.yaml | 4 ++-- templates/standard/conda.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index c00993f2..e0078d4f 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -1,11 +1,11 @@ channels: # Define conda channels here. - - defaults - conda-forge + - defaults dependencies: # Define conda packages here. - # We recommend using packages from the conda-forge channel. + # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - python=3.7.5 diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index c00993f2..e0078d4f 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -1,11 +1,11 @@ channels: # Define conda channels here. - - defaults - conda-forge + - defaults dependencies: # Define conda packages here. - # We recommend using packages from the conda-forge channel. + # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - python=3.7.5 diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index c00993f2..e0078d4f 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -1,11 +1,11 @@ channels: # Define conda channels here. - - defaults - conda-forge + - defaults dependencies: # Define conda packages here. - # We recommend using packages from the conda-forge channel. + # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - python=3.7.5 From 87d1515bec9b0233d9dcf36d30b6eed34ff55446 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Fri, 8 Jan 2021 16:46:14 +0200 Subject: [PATCH 042/516] v8.0.6 --- common/version.go | 2 +- docs/changelog.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 538d5d7d..9b5c3e15 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.5` + Version = `v8.0.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2b15416c..a32ff7df 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v8.0.6 (date: 8.1.2021) + +- Updated to robot templates +- conda channels in order for `--strict-channel-priority` +- library versions updated and strict as well (rpaframework v7.1.1) +- Added basic guides for what to do in conda.yaml for end-users. + ## v8.0.5 (date: 8.1.2021) - added robot test to validate required changes, which are common/version.go From 23fbed8a943d69233c3be30e08719edcb8ba9968 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 11 Jan 2021 11:52:10 +0200 Subject: [PATCH 043/516] RCC-124: micromamba -- temp folder handling (v8.0.7) - Now rcc manages TEMP and TMP locations for its subprocesses --- cmd/rcc/main.go | 32 ++++++++++++++++++++++++++++++++ common/version.go | 2 +- conda/cleanup.go | 6 ++++++ conda/platform_darwin_amd64.go | 3 +++ conda/platform_linux_amd64.go | 3 +++ conda/platform_windows_amd64.go | 11 +++-------- conda/robocorp.go | 29 +++++++++++++++++++++++++---- docs/changelog.md | 4 ++++ robot/config.go | 2 ++ robot/robot.go | 2 ++ robot_tests/fullrun.robot | 4 ++++ 11 files changed, 85 insertions(+), 13 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 2573fbf7..a5de40bd 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -1,16 +1,24 @@ package main import ( + "io/ioutil" "os" + "path/filepath" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" +) + +var ( + markedAlready = false ) func ExitProtection() { status := recover() if status != nil { + markTempForRecycling() exit, ok := status.(common.ExitCode) if ok { exit.ShowMessage() @@ -24,7 +32,31 @@ func ExitProtection() { cloud.WaitTelemetry() } +func startTempRecycling() { + pattern := filepath.Join(conda.RobocorpTempRoot(), "*", "recycle.now") + found, err := filepath.Glob(pattern) + if err != nil { + common.Debug("Recycling failed, reason: %v", err) + return + } + for _, filename := range found { + go os.RemoveAll(filepath.Dir(filename)) + } +} + +func markTempForRecycling() { + if markedAlready { + return + } + markedAlready = true + filename := filepath.Join(conda.RobocorpTemp(), "recycle.now") + ioutil.WriteFile(filename, []byte("True"), 0o640) + common.Debug("Marked %q for recyling.", conda.RobocorpTemp()) +} + func main() { + go startTempRecycling() + defer markTempForRecycling() defer os.Stderr.Sync() defer os.Stdout.Sync() defer ExitProtection() diff --git a/common/version.go b/common/version.go index 9b5c3e15..adbdc4eb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.6` + Version = `v8.0.7` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 2b0835f9..689b72e8 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -54,6 +54,7 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", PipCache()) common.Log("- %v", MambaPackages()) common.Log("- %v", BinMicromamba()) + common.Log("- %v", RobocorpTempRoot()) return nil } err := os.RemoveAll(TemplateLocation()) @@ -81,6 +82,11 @@ func spotlessCleanup(dryrun bool) error { return err } common.Debug("Removed executable %v.", BinMicromamba()) + err = os.RemoveAll(RobocorpTempRoot()) + if err != nil { + return err + } + common.Debug("Removed directory %v.", RobocorpTempRoot()) return nil } diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 3511e39d..79442cbb 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -29,6 +29,9 @@ func ExpandPath(entry string) string { func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + tempFolder := RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env } diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 0a3b4df7..3027a89f 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -33,6 +33,9 @@ func ExpandPath(entry string) string { func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + tempFolder := RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env } diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 73aaa7e2..619cc0a2 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -62,14 +62,9 @@ func ensureHardlinkEnvironmment() (string, error) { func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) - /* - path, err := ensureHardlinkEnvironmment() - if err != nil { - return nil - } - env = append(env, fmt.Sprintf("TEMP=%s", path)) - env = append(env, fmt.Sprintf("TMP=%s", path)) - */ + tempFolder := RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env } diff --git a/conda/robocorp.go b/conda/robocorp.go index 4bd5a161..033bd75a 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -3,6 +3,7 @@ package conda import ( "crypto/sha256" "fmt" + "math/rand" "os" "path/filepath" "regexp" @@ -20,11 +21,16 @@ const ( ) var ( - ignoredPaths = []string{"python", "conda"} - pythonPaths = []string{"resources", "libraries", "tasks", "variables"} - hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") + ignoredPaths = []string{"python", "conda"} + pythonPaths = []string{"resources", "libraries", "tasks", "variables"} + hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") + randomIdentifier string ) +func init() { + randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) +} + func sorted(files []os.FileInfo) { sort.SliceStable(files, func(left, right int) bool { return files[left].Name() < files[right].Name() @@ -163,6 +169,8 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+RobocorpHome(), + "TEMP="+RobocorpTemp(), + "TMP="+RobocorpTemp(), searchPath.AsEnvironmental("PATH"), PythonPath().AsEnvironmental("PYTHONPATH"), ) @@ -203,7 +211,7 @@ func HasMicroMamba() bool { if !pathlib.IsFile(BinMicromamba()) { return false } - versionText, _, err := shell.New(nil, ".", BinMicromamba(), "--version").CaptureOutput() + versionText, _, err := shell.New(CondaEnvironment(), ".", BinMicromamba(), "--version").CaptureOutput() if err != nil { return false } @@ -221,6 +229,19 @@ func RobocorpHome() string { return ExpandPath(defaultRobocorpLocation) } +func RobocorpTempRoot() string { + return filepath.Join(RobocorpHome(), "temp") +} + +func RobocorpTemp() string { + tempLocation := filepath.Join(RobocorpTempRoot(), randomIdentifier) + fullpath, err := pathlib.EnsureDirectory(tempLocation) + if err != nil { + common.Log("WARNING (%v) -> %v", tempLocation, err) + } + return fullpath +} + func BinLocation() string { return filepath.Join(RobocorpHome(), "bin") } diff --git a/docs/changelog.md b/docs/changelog.md index a32ff7df..ec6c58ec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v8.0.7 (date: 11.1.2021) + +- Now rcc manages TEMP and TMP locations for its subprocesses + ## v8.0.6 (date: 8.1.2021) - Updated to robot templates diff --git a/robot/config.go b/robot/config.go index 88e49a30..d6023137 100644 --- a/robot/config.go +++ b/robot/config.go @@ -237,6 +237,8 @@ func (it *Activity) ExecutionEnvironment(base Robot, location string, inject []s "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+conda.RobocorpHome(), + "TEMP="+conda.RobocorpTemp(), + "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), pythonPath.AsEnvironmental("PYTHONPATH"), fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory(base)), diff --git a/robot/robot.go b/robot/robot.go index adfc1d94..99b01edf 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -219,6 +219,8 @@ func (it *robot) ExecutionEnvironment(taskname, location string, inject []string "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+conda.RobocorpHome(), + "TEMP="+conda.RobocorpTemp(), + "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), it.PythonPaths("").AsEnvironmental("PYTHONPATH"), fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory("")), diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index bfd02fba..90beb847 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -114,6 +114,8 @@ Using and running template example with shell file Must Have PYTHONHOME= Must Have PYTHONEXECUTABLE= Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= Must Have f0a9e281269b31ea @@ -135,6 +137,8 @@ Using and running template example with shell file Must Have PYTHONHOME= Must Have PYTHONEXECUTABLE= Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= Must Have ROBOT_ROOT= Must Have ROBOT_ARTIFACTS= Wont Have RC_API_SECRET_HOST= From 0641ca55f7a56b942da56a84c1ff85f11570ddd6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 14 Jan 2021 07:54:14 +0200 Subject: [PATCH 044/516] RCC-131: micromamba handling fixes (v8.0.8) - now micromamba 0.7.8 is required - repodata TTL is reduced to 16 hours, and in case of environment creation failure, fall back to 0 seconds TTL (immediate update) - using new --retry-with-clean-cache option in micromamba --- common/version.go | 2 +- conda/robocorp.go | 2 +- conda/validate.go | 5 ++--- conda/workflows.go | 8 ++++++-- docs/changelog.md | 7 +++++++ 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index adbdc4eb..634f4e56 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.7` + Version = `v8.0.8` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 033bd75a..ab056e69 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -216,7 +216,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(versionText) - goodEnough := version >= 7007 + goodEnough := version >= 7008 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/conda/validate.go b/conda/validate.go index 51a2ca83..d44e1d48 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -1,7 +1,6 @@ package conda import ( - "os" "regexp" "github.com/robocorp/rcc/common" @@ -35,8 +34,8 @@ func validateLocations(checked map[string]string) bool { func ValidateLocations() bool { checked := map[string]string{ - "Environment variable 'TMP'": os.Getenv("TMP"), - "Environment variable 'TEMP'": os.Getenv("TEMP"), + //"Environment variable 'TMP'": os.Getenv("TMP"), + //"Environment variable 'TEMP'": os.Getenv("TEMP"), "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), } // 7.1.2021 -- just warnings for now -- JMP:FIXME:JMP later diff --git a/conda/workflows.go b/conda/workflows.go index e461ca80..dbcf48a9 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -136,9 +136,13 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) - command := []string{BinMicromamba(), "create", "--strict-channel-priority", "--repodata-ttl", "604800", "-q", "-y", "-f", condaYaml, "-p", targetFolder} + ttl := "57600" + if force { + ttl = "0" + } + command := []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{BinMicromamba(), "create", "--strict-channel-priority", "--repodata-ttl", "604800", "-v", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-v", "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index ec6c58ec..83e98a98 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v8.0.8 (date: 15.1.2021) + +- now micromamba 0.7.8 is required +- repodata TTL is reduced to 16 hours, and in case of environment creation + failure, fall back to 0 seconds TTL (immediate update) +- using new --retry-with-clean-cache option in micromamba + ## v8.0.7 (date: 11.1.2021) - Now rcc manages TEMP and TMP locations for its subprocesses From ad287fb6017d289e661385fca65735cb08acbfc9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 15 Jan 2021 09:24:35 +0200 Subject: [PATCH 045/516] RCC-131: micromamba handling fixes (v8.0.9) - fix: removing one verbosity flag from micromamba invocation --- common/version.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 634f4e56..1a0a95a4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.8` + Version = `v8.0.9` ) diff --git a/conda/workflows.go b/conda/workflows.go index dbcf48a9..10ca1760 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -142,7 +142,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } command := []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-v", "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 83e98a98..a5f066bc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v8.0.9 (date: 15.1.2021) + +- fix: removing one verbosity flag from micromamba invocation + ## v8.0.8 (date: 15.1.2021) - now micromamba 0.7.8 is required From 873ad8ffff405e1ed09f88ca280efa6688493abe Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 18 Jan 2021 09:45:56 +0200 Subject: [PATCH 046/516] RCC-118: pip fails when no pip dependencies (v8.0.10) - fix: when there is no pip dependencies, do not try to run pip command --- common/version.go | 2 +- conda/workflows.go | 28 +++++++++++++++++----------- docs/changelog.md | 4 ++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/common/version.go b/common/version.go index 1a0a95a4..06290a9c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.9` + Version = `v8.0.10` ) diff --git a/conda/workflows.go b/conda/workflows.go index 10ca1760..07d1859c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -154,17 +154,23 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if observer.HasFailures(targetFolder) { return false, true } - common.Debug("Updating new environment at %v with pip requirements from %v", targetFolder, requirementsText) - pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", PipCache(), "--find-links", WheelCache(), "--requirement", requirementsText, "--quiet"} - if common.DebugFlag { - pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", PipCache(), "--find-links", WheelCache(), "--requirement", requirementsText} - } - common.Log("#### Progress: 3/5 [pip install phase]") - common.Debug("=== new live --- pip install phase ===") - err = LiveExecution(targetFolder, pipCommand...) - if err != nil { - common.Error("Pip error", err) - return false, false + pipCache, wheelCache := PipCache(), WheelCache() + size, ok := pathlib.Size(requirementsText) + if !ok || size == 0 { + common.Log("#### Progress: 3/5 [pip install phase skipped -- no pip dependencies]") + } else { + common.Log("#### Progress: 3/5 [pip install phase]") + common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) + pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet"} + if common.DebugFlag { + pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText} + } + common.Debug("=== new live --- pip install phase ===") + err = LiveExecution(targetFolder, pipCommand...) + if err != nil { + common.Error("Pip error", err) + return false, false + } } if postInstall != nil && len(postInstall) > 0 { common.Debug("=== new live --- post install phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index a5f066bc..0c30fa2f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v8.0.10 (date: 18.1.2021) + +- fix: when there is no pip dependencies, do not try to run pip command + ## v8.0.9 (date: 15.1.2021) - fix: removing one verbosity flag from micromamba invocation From 0c720fe0e7645a2825b05666ebac4622017e0e05 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 15 Jan 2021 09:15:31 +0200 Subject: [PATCH 047/516] RCC-126: environment leasing (v9.0.0) - BREAKING CHANGES - new cli option `--lease` to request longer lasting environment (1 hour from lease request, and next requests refresh the lease) - new environment variable: `RCC_ENVIRONMENT_HASH` for clients to use - new command `rcc env unlease` to stop leasing environments - this breaks contract of pristine environments in cases where one application has already requested long living lease, and other wants to use environment with exactly same specification (if pristine, it is shared, otherwise it is an error) --- cmd/env.go | 1 + cmd/envUnlease.go | 37 +++++++++ cmd/list.go | 4 +- common/variables.go | 22 +++-- common/version.go | 2 +- conda/cleanup.go | 17 ++++ conda/lease.go | 140 ++++++++++++++++++++++++++++++++ conda/robocorp.go | 5 +- conda/workflows.go | 10 +++ docs/changelog.md | 12 +++ robot/config.go | 2 + robot/robot.go | 1 + robot_tests/fullrun.robot | 2 +- robot_tests/lease.robot | 72 ++++++++++++++++ robot_tests/leasebot/.gitignore | 13 +++ robot_tests/leasebot/conda.yaml | 5 ++ robot_tests/leasebot/robot.yaml | 12 +++ 17 files changed, 343 insertions(+), 14 deletions(-) create mode 100644 cmd/envUnlease.go create mode 100644 conda/lease.go create mode 100644 robot_tests/lease.robot create mode 100755 robot_tests/leasebot/.gitignore create mode 100755 robot_tests/leasebot/conda.yaml create mode 100755 robot_tests/leasebot/robot.yaml diff --git a/cmd/env.go b/cmd/env.go index fa65a0a6..e07aa52b 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -15,5 +15,6 @@ used in task context locally.`, func init() { rootCmd.AddCommand(envCmd) + envCmd.PersistentFlags().StringVar(&common.LeaseContract, "lease", "", "unique lease contract for long living environments") envCmd.PersistentFlags().StringVar(&common.StageFolder, "stage", "", "internal, DO NOT USE (unless you know what you are doing)") } diff --git a/cmd/envUnlease.go b/cmd/envUnlease.go new file mode 100644 index 00000000..43662f50 --- /dev/null +++ b/cmd/envUnlease.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + leaseHash string +) + +var envUnleaseCmd = &cobra.Command{ + Use: "unlease", + Short: "Drop existing lease of given environment.", + Long: "Drop existing lease of given environment.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Conda env unlease lasted").Report() + } + err := conda.DropLease(leaseHash, common.LeaseContract) + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + pretty.Ok() + }, +} + +func init() { + envCmd.AddCommand(envUnleaseCmd) + envUnleaseCmd.Flags().StringVar(&common.LeaseContract, "lease", "", "unique lease contract for long living environments") + envUnleaseCmd.MarkFlagRequired("lease") + envUnleaseCmd.Flags().StringVar(&leaseHash, "hash", "", "hash identity of leased environment") + envUnleaseCmd.MarkFlagRequired("hash") +} diff --git a/cmd/list.go b/cmd/list.go index 05ca8ccf..2964b7cb 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -23,7 +23,7 @@ in human readable form.`, pretty.Exit(1, "No environments available.") } lines := make([]string, 0, len(templates)) - common.Log("%-25s %-25s %s", "Last used", "Last cloned", "Environment") + common.Log("%-25s %-25s %-16s %s", "Last used", "Last cloned", "Environment", "Leased duration") for _, template := range templates { cloned := "N/A" used := cloned @@ -35,7 +35,7 @@ in human readable form.`, if err == nil { used = when.Format(time.RFC3339) } - lines = append(lines, fmt.Sprintf("%-25s %-25s %s", used, cloned, template)) + lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %q %s", used, cloned, template, conda.WhoLeased(template), conda.LeaseExpires(template))) } sort.Strings(lines) for _, line := range lines { diff --git a/common/variables.go b/common/variables.go index 7ac4dac3..68ee6b2e 100644 --- a/common/variables.go +++ b/common/variables.go @@ -6,14 +6,16 @@ import ( ) var ( - Silent bool - DebugFlag bool - TraceFlag bool - NoCache bool - Liveonly bool - Stageonly bool - StageFolder string - ControllerType string + Silent bool + DebugFlag bool + TraceFlag bool + NoCache bool + Liveonly bool + Stageonly bool + StageFolder string + ControllerType string + LeaseContract string + EnvironmentHash string ) const ( @@ -46,3 +48,7 @@ func ForceDebug() { func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } + +func IsLeaseRequest() bool { + return len(LeaseContract) > 0 +} diff --git a/common/version.go b/common/version.go index 06290a9c..2d18204e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v8.0.10` + Version = `v9.0.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 689b72e8..bdd629b7 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -1,6 +1,7 @@ package conda import ( + "fmt" "os" "time" @@ -47,6 +48,9 @@ func orphanCleanup(dryrun bool) error { } func spotlessCleanup(dryrun bool) error { + if anyLeasedEnvironment() { + return fmt.Errorf("Cannot clean everything, since there are some leased environments!") + } if dryrun { common.Log("Would be removing:") common.Log("- %v", TemplateLocation()) @@ -90,6 +94,15 @@ func spotlessCleanup(dryrun bool) error { return nil } +func anyLeasedEnvironment() bool { + for _, template := range TemplateList() { + if IsLeasedEnvironment(template) { + return true + } + } + return false +} + func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) error { lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) @@ -120,6 +133,10 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) err if !all && whenBase.After(deadline) { continue } + if IsLeasedEnvironment(template) { + common.Log("WARNING: %q is leased by %q and wont be cleaned up!", template, WhoLeased(template)) + continue + } if dryrun { common.Log("Would be removing %v.", template) continue diff --git a/conda/lease.go b/conda/lease.go new file mode 100644 index 00000000..15484e76 --- /dev/null +++ b/conda/lease.go @@ -0,0 +1,140 @@ +package conda + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" +) + +func LeaseInterceptor(hash string) (string, bool, error) { + if !common.IsLeaseRequest() { + err := DoesLeasingAllowUsage(hash) + if err != nil { + return "", false, err + } + } else { + leased := IsLeasedEnvironment(hash) + if leased && CouldExtendLease(hash) { + common.Debug("Lease of %q was extended for %q!", hash, WhoLeased(hash)) + return LiveFrom(hash), true, nil + } + if leased && !IsLeasePristine(hash) { + return "", false, fmt.Errorf("Cannot get environment %q because it is dirty and leased by %q!", hash, WhoLeased(hash)) + } + if !IsLeasedEnvironment(hash) { + err := TakeLease(hash, common.LeaseContract) + if err != nil { + return "", false, err + } + common.Debug("Lease of %q taken by %q!", hash, WhoLeased(hash)) + } + } + return "", false, nil +} + +func DoesLeasingAllowUsage(hash string) error { + if !IsLeasedEnvironment(hash) || IsLeasePristine(hash) { + return nil + } + reason, err := readLeaseFile(hash) + if err != nil { + return err + } + return fmt.Errorf("Environment leased to %q is dirty! Cannot use it for now!", reason) +} + +func CouldExtendLease(hash string) bool { + reason, err := readLeaseFile(hash) + if err != nil { + return false + } + if reason != common.LeaseContract { + return false + } + pathlib.TouchWhen(LeaseFileFrom(hash), time.Now()) + return true +} + +func TakeLease(hash, reason string) error { + return writeLeaseFile(hash, reason) +} + +func DropLease(hash, reason string) error { + if !IsLeasedEnvironment(hash) { + return fmt.Errorf("Not a leased environment: %q!", hash) + } + if reason != WhoLeased(hash) { + return fmt.Errorf("Environment %q is not leased by %q!", hash, reason) + } + return os.Remove(LeaseFileFrom(hash)) +} + +func WhoLeased(hash string) string { + if !IsLeasedEnvironment(hash) { + return "~" + } + reason, err := readLeaseFile(hash) + if err != nil { + return err.Error() + } + return reason +} + +func LeaseExpires(hash string) time.Duration { + leasefile := LeaseFileFrom(hash) + stamp, err := pathlib.Modtime(leasefile) + if err != nil { + return 0 * time.Second + } + deadline := stamp.Add(1 * time.Hour) + delta := deadline.Sub(time.Now()).Round(1 * time.Second) + if delta < 0*time.Second { + return 0 * time.Second + } + return delta +} + +func IsLeasedEnvironment(hash string) bool { + leasefile := LeaseFileFrom(hash) + exists := pathlib.IsFile(leasefile) + if !exists { + return false + } + return LeaseExpires(hash) > 0*time.Second +} + +func IsLeasePristine(hash string) bool { + return IsPristine(LiveFrom(hash)) +} + +func LeaseFileFrom(hash string) string { + return fmt.Sprintf("%s.lease", LiveFrom(hash)) +} + +func IsSameLease(hash, reason string) bool { + if !IsLeasedEnvironment(hash) { + return false + } + content, err := readLeaseFile(hash) + return err == nil && content == reason +} + +func readLeaseFile(hash string) (string, error) { + leasefile := LeaseFileFrom(hash) + content, err := ioutil.ReadFile(leasefile) + if err != nil { + return "", err + } + return string(content), nil +} + +func writeLeaseFile(hash, reason string) error { + leasefile := LeaseFileFrom(hash) + flatReason := strings.TrimSpace(reason) + return ioutil.WriteFile(leasefile, []byte(flatReason), 0o640) +} diff --git a/conda/robocorp.go b/conda/robocorp.go index ab056e69..9afd7a5a 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -169,6 +169,7 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+RobocorpHome(), + "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "TEMP="+RobocorpTemp(), "TMP="+RobocorpTemp(), searchPath.AsEnvironmental("PATH"), @@ -247,11 +248,11 @@ func BinLocation() string { } func LiveLocation() string { - return filepath.Join(RobocorpHome(), "live") + return ensureDirectory(filepath.Join(RobocorpHome(), "live")) } func TemplateLocation() string { - return filepath.Join(RobocorpHome(), "base") + return ensureDirectory(filepath.Join(RobocorpHome(), "base")) } func RobocorpLock() string { diff --git a/conda/workflows.go b/conda/workflows.go index 07d1859c..e437dcb1 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -301,6 +301,16 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { defer os.Remove(condaYaml) defer os.Remove(requirementsText) + common.EnvironmentHash = key + + quickFolder, ok, err := LeaseInterceptor(key) + if ok { + return quickFolder, nil + } + if err != nil { + return "", err + } + liveFolder := LiveFrom(key) if reuseExistingLive(key) { hits += 1 diff --git a/docs/changelog.md b/docs/changelog.md index 0c30fa2f..0f1c5172 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,17 @@ # rcc change log +## v9.0.0 (date: 18.1.2021) + +- BREAKING CHANGES +- new cli option `--lease` to request longer lasting environment (1 hour from + lease request, and next requests refresh the lease) +- new environment variable: `RCC_ENVIRONMENT_HASH` for clients to use +- new command `rcc env unlease` to stop leasing environments +- this breaks contract of pristine environments in cases where one application + has already requested long living lease, and other wants to use environment + with exactly same specification (if pristine, it is shared, otherwise it is + an error) + ## v8.0.10 (date: 18.1.2021) - fix: when there is no pip dependencies, do not try to run pip command diff --git a/robot/config.go b/robot/config.go index d6023137..00212eb0 100644 --- a/robot/config.go +++ b/robot/config.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" @@ -237,6 +238,7 @@ func (it *Activity) ExecutionEnvironment(base Robot, location string, inject []s "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+conda.RobocorpHome(), + "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "TEMP="+conda.RobocorpTemp(), "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), diff --git a/robot/robot.go b/robot/robot.go index 99b01edf..cd732f92 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -219,6 +219,7 @@ func (it *robot) ExecutionEnvironment(taskname, location string, inject []string "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+conda.RobocorpHome(), + "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "TEMP="+conda.RobocorpTemp(), "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 90beb847..6da51cd9 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -9,7 +9,7 @@ Using and running template example with shell file Goal Show rcc version information. Step build/rcc version --controller citests - Must Have v8. + Must Have v9. Goal Show rcc license information. Step build/rcc man license --controller citests diff --git a/robot_tests/lease.robot b/robot_tests/lease.robot new file mode 100644 index 00000000..4a7c1fd9 --- /dev/null +++ b/robot_tests/lease.robot @@ -0,0 +1,72 @@ +*** Settings *** +Resource resources.robot + +*** Test cases *** +Can operate leased environments + + Goal Create environment with lease + Step build/rcc env variables --lease "taker (1)" robot_tests/leasebot/conda.yaml + Must Have CONDA_DEFAULT_ENV=rcc + Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + + Goal Check listing for taker information + Step build/rcc env list + Must Have "taker (1)" + + Goal Others can get same environment + Step build/rcc env variables robot_tests/leasebot/conda.yaml + Must Have CONDA_DEFAULT_ENV=rcc + Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + + Goal Can share environment, but wont own the lease + Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml + Must Have CONDA_DEFAULT_ENV=rcc + Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + + Goal Check listing for taker information (still same) + Step build/rcc env list + Must Have "taker (1)" + Wont Have "second (2)" + + Goal Lets corrupt the environment + Step build/rcc task run -r robot_tests/leasebot/robot.yaml + Must Have Successfully installed pytz + + Goal Now others cannot get same environment anymore + Step build/rcc env variables robot_tests/leasebot/conda.yaml 1 + Must Have Environment leased to "taker (1)" is dirty + Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + Wont Have CONDA_DEFAULT_ENV=rcc + + Goal Cannot share environment, since it is dirty + Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml 1 + Must Have Cannot get environment "8f1d3dc95228edef" because it is dirty and leased by "taker (1)" + Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + Wont Have CONDA_DEFAULT_ENV=rcc + + Goal Cannot unlease someone elses environment + Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef 1 + Must Have Error: + + Goal Check listing for taker information (still same) + Step build/rcc env list + Must Have "taker (1)" + Wont Have "second (2)" + + Goal Lease can be unleased + Step build/rcc env unlease --lease "taker (1)" --hash 8f1d3dc95228edef + Must Have OK. + + Goal Others can now lease that environment + Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml + Must Have CONDA_DEFAULT_ENV=rcc + Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef + + Goal Check listing for taker information (still same) + Step build/rcc env list + Must Have "second (2)" + Wont Have "taker (1)" + + Goal Lease can be unleased + Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef + Must Have OK. diff --git a/robot_tests/leasebot/.gitignore b/robot_tests/leasebot/.gitignore new file mode 100755 index 00000000..09bd393b --- /dev/null +++ b/robot_tests/leasebot/.gitignore @@ -0,0 +1,13 @@ +testrun/ +output/ +venv/ +temp/ +.rpa/ +.idea/ +.ipynb_checkpoints/ +.virtual_documents/ +*/.ipynb_checkpoints/* +.vscode +.DS_Store +*.pyc +*.zip diff --git a/robot_tests/leasebot/conda.yaml b/robot_tests/leasebot/conda.yaml new file mode 100755 index 00000000..d1cae0c8 --- /dev/null +++ b/robot_tests/leasebot/conda.yaml @@ -0,0 +1,5 @@ +channels: +- conda-forge +dependencies: +- python=3.8.6 +- pip=20.1 diff --git a/robot_tests/leasebot/robot.yaml b/robot_tests/leasebot/robot.yaml new file mode 100755 index 00000000..10b1f959 --- /dev/null +++ b/robot_tests/leasebot/robot.yaml @@ -0,0 +1,12 @@ +tasks: + corrupt: + shell: pip install pytz + +condaConfigFile: conda.yaml +artifactsDir: output +PATH: + - . +PYTHONPATH: + - . +ignoreFiles: + - .gitignore From cc537a8d298567a61b62fb80315f2c1421f9337b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Jan 2021 11:50:53 +0200 Subject: [PATCH 048/516] RCC-57: legacy package.yaml support removal (v9.0.1) fixup! RCC-57: legacy package.yaml support removal (v9.0.1) --- cmd/assistantRun.go | 2 +- cmd/variables.go | 2 +- common/version.go | 2 +- docs/changelog.md | 5 + operations/fixing.go | 9 +- operations/running.go | 2 +- operations/zipper.go | 2 +- robot/config.go | 275 ------------------------------------------ robot/config_test.go | 134 -------------------- robot/robot.go | 71 +++++------ robot/robot_test.go | 10 +- 11 files changed, 51 insertions(+), 463 deletions(-) delete mode 100644 robot/config.go delete mode 100644 robot/config_test.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 712f5b21..8726f845 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -78,7 +78,7 @@ var assistantRunCmd = &cobra.Command{ reason = "SETUP_FAILURE" targetRobot := robot.DetectConfigurationName(workarea) simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, assistant.TaskName, forceFlag) - artifactDir := config.ArtifactDirectory("") + artifactDir := config.ArtifactDirectory() if len(copyDirectory) > 0 && len(artifactDir) > 0 { err := os.MkdirAll(copyDirectory, 0o755) if err == nil { diff --git a/cmd/variables.go b/cmd/variables.go index ab9c03f7..42ef8703 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -59,7 +59,7 @@ func exportEnvironment(condaYaml []string, packfile, taskName, environment, work var data operations.Token if Has(packfile) { - config, err = robot.LoadYamlConfiguration(packfile) + config, err = robot.LoadRobotYaml(packfile) if err == nil { condaYaml = append(condaYaml, config.CondaConfigFile()) task = config.TaskByName(taskName) diff --git a/common/version.go b/common/version.go index 2d18204e..d6846891 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.0.0` + Version = `v9.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0f1c5172..66da68c3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.0.1 (date: 20.1.2021) + +- BREAKING CHANGES +- removal of legacy "package.yaml" support + ## v9.0.0 (date: 18.1.2021) - BREAKING CHANGES diff --git a/operations/fixing.go b/operations/fixing.go index 43de9731..2aedfc88 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -75,15 +75,12 @@ func ensureFilesExecutable(dir string) { } func FixRobot(robotFile string) error { - config, err := robot.LoadYamlConfiguration(robotFile) + config, err := robot.LoadRobotYaml(robotFile) if err != nil { return err } - tasks := config.AvailableTasks() - for _, task := range tasks { - for _, path := range config.Paths(task) { - ensureFilesExecutable(path) - } + for _, path := range config.Paths() { + ensureFilesExecutable(path) } return nil } diff --git a/operations/running.go b/operations/running.go index b74f9fe1..21895afa 100644 --- a/operations/running.go +++ b/operations/running.go @@ -46,7 +46,7 @@ func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, enviro func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot.Robot, robot.Task, string) { FixRobot(packfile) - config, err := robot.LoadYamlConfiguration(packfile) + config, err := robot.LoadRobotYaml(packfile) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/operations/zipper.go b/operations/zipper.go index 499f878b..358b70fa 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -232,7 +232,7 @@ func Unzip(directory, zipfile string, force, temporary bool) error { func Zip(directory, zipfile string, ignores []string) error { common.Debug("Wrapping %v into %v ...", directory, zipfile) - config, err := robot.LoadYamlConfiguration(robot.DetectConfigurationName(directory)) + config, err := robot.LoadRobotYaml(robot.DetectConfigurationName(directory)) if err != nil { return err } diff --git a/robot/config.go b/robot/config.go deleted file mode 100644 index 00212eb0..00000000 --- a/robot/config.go +++ /dev/null @@ -1,275 +0,0 @@ -package robot - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pathlib" - - "gopkg.in/yaml.v2" -) - -type Config struct { - Activities map[string]*Activity `yaml:"activities"` - Conda string `yaml:"condaConfig"` - Ignored []string `yaml:"ignoreFiles"` - Root string -} - -type Activity struct { - Output string `yaml:"output"` - Root string `yaml:"activityRoot"` - Environment *Environment `yaml:"environment"` - Action *Action `yaml:"action"` -} - -type Environment struct { - Path []string `yaml:"path"` - PythonPath []string `yaml:"pythonPath"` -} - -type Action struct { - Command []string -} - -func (it *Config) RootDirectory() string { - return it.Root -} - -func (it *Config) IgnoreFiles() []string { - if it.Ignored == nil { - return []string{} - } - result := make([]string, 0, len(it.Ignored)) - for _, entry := range it.Ignored { - result = append(result, filepath.Join(it.Root, entry)) - } - return result -} - -func (it *Config) AvailableTasks() []string { - result := make([]string, 0, len(it.Activities)) - for name, _ := range it.Activities { - result = append(result, name) - } - sort.Strings(result) - return result -} - -func (it *Config) DefaultTask() Task { - if len(it.Activities) != 1 { - return nil - } - var result *Activity - for _, value := range it.Activities { - result = value - break - } - return result -} - -func (it *Config) TaskByName(key string) Task { - if len(key) == 0 { - return it.DefaultTask() - } - found, ok := it.Activities[key] - if ok { - return found - } - caseless := strings.ToLower(key) - for name, value := range it.Activities { - if caseless == strings.ToLower(name) { - return value - } - } - return nil -} - -func (it *Config) UsesConda() bool { - return len(it.Conda) > 0 -} - -func (it *Config) CondaConfigFile() string { - return filepath.Join(it.Root, it.Conda) -} - -func (it *Config) WorkingDirectory(taskname string) string { - activity := it.TaskByName(taskname) - if activity == nil { - return "" - } - return activity.WorkingDirectory(it) -} - -func (it *Config) ArtifactDirectory(taskname string) string { - activity := it.TaskByName(taskname) - if activity == nil { - return "" - } - return activity.ArtifactDirectory(it) -} - -func (it *Config) Paths(taskname string) pathlib.PathParts { - activity := it.TaskByName(taskname) - if activity == nil { - return pathlib.PathFrom() - } - return activity.Paths(it) -} - -func (it *Config) PythonPaths(taskname string) pathlib.PathParts { - activity := it.TaskByName(taskname) - if activity == nil { - return pathlib.PathFrom() - } - return activity.PythonPaths(it) -} - -func (it *Config) SearchPath(taskname, location string) pathlib.PathParts { - activity := it.TaskByName(taskname) - if activity == nil { - return pathlib.PathFrom() - } - return activity.SearchPath(it, location) -} - -func (it *Config) ExecutionEnvironment(taskname, location string, inject []string, full bool) []string { - activity := it.TaskByName(taskname) - if activity == nil { - return []string{} - } - return activity.ExecutionEnvironment(it, location, inject, full) -} - -func (it *Config) Validate() (bool, error) { - if it.Activities == nil { - return false, errors.New("In package.yaml, 'activities:' is required!") - } - if len(it.Activities) == 0 { - return false, errors.New("In package.yaml, 'activities:' must have at least one activity defined!") - } - if it.Conda == "" { - return false, errors.New("In package.yaml, 'condaConfig:' is required!") - } - for name, activity := range it.Activities { - if activity.Output == "" { - return false, fmt.Errorf("In package.yaml, 'output:' is required for activity %s!", name) - } - if activity.Root == "" { - return false, fmt.Errorf("In package.yaml, 'activityRoot:' is required for activity %s!", name) - } - if activity.Action == nil { - return false, fmt.Errorf("In package.yaml, 'action:' is required for activity %s!", name) - } - if activity.Action.Command == nil { - return false, fmt.Errorf("In package.yaml, 'action/command:' is required for activity %s!", name) - } - if len(activity.Action.Command) == 0 { - return false, fmt.Errorf("In package.yaml, 'action/command:' cannot be empty for activity %s!", name) - } - } - return true, nil -} - -func (it *Activity) Commandline() []string { - return it.Action.Command -} - -func (it *Activity) WorkingDirectory(base Robot) string { - return filepath.Join(base.RootDirectory(), it.Root) -} - -func (it *Activity) ArtifactDirectory(base Robot) string { - return filepath.Join(it.WorkingDirectory(base), it.Output) -} - -func (it *Activity) Paths(base Robot) pathlib.PathParts { - if it.Environment == nil { - return pathlib.PathFrom() - } - return pathBuilder(base.RootDirectory(), it.Environment.Path) -} - -func (it *Activity) PythonPaths(base Robot) pathlib.PathParts { - if it.Environment == nil { - return pathlib.PathFrom() - } - return pathBuilder(base.RootDirectory(), it.Environment.PythonPath) -} - -func (it *Activity) SearchPath(base Robot, location string) pathlib.PathParts { - return conda.FindPath(location).Prepend(it.Paths(base)...) -} - -func PlainEnvironment(inject []string, full bool) []string { - environment := make([]string, 0, 100) - if full { - environment = append(environment, os.Environ()...) - } - environment = append(environment, inject...) - return environment -} - -func (it *Activity) ExecutionEnvironment(base Robot, location string, inject []string, full bool) []string { - pythonPath := it.PythonPaths(base) - environment := PlainEnvironment(inject, full) - searchPath := it.SearchPath(base, location) - python, ok := searchPath.Which("python3", conda.FileExtensions) - if !ok { - python, ok = searchPath.Which("python", conda.FileExtensions) - } - if ok { - environment = append(environment, "PYTHON_EXE="+python) - } - return append(environment, - "CONDA_DEFAULT_ENV=rcc", - "CONDA_PREFIX="+location, - "CONDA_PROMPT_MODIFIER=(rcc)", - "CONDA_SHLVL=1", - "PYTHONHOME=", - "PYTHONSTARTUP=", - "PYTHONEXECUTABLE=", - "PYTHONNOUSERSITE=1", - "ROBOCORP_HOME="+conda.RobocorpHome(), - "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, - "TEMP="+conda.RobocorpTemp(), - "TMP="+conda.RobocorpTemp(), - searchPath.AsEnvironmental("PATH"), - pythonPath.AsEnvironmental("PYTHONPATH"), - fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory(base)), - fmt.Sprintf("ROBOT_ARTIFACTS=%s", it.ArtifactDirectory(base)), - ) -} - -func ActivityPackageFrom(content []byte) (*Config, error) { - config := Config{} - err := yaml.Unmarshal(content, &config) - if err != nil { - return nil, err - } - return &config, nil -} - -func LoadActivityPackage(filename string) (Robot, error) { - fullpath, err := filepath.Abs(filename) - if err != nil { - return nil, err - } - content, err := ioutil.ReadFile(fullpath) - if err != nil { - return nil, err - } - config, err := ActivityPackageFrom(content) - if err != nil { - return nil, err - } - config.Root = filepath.Dir(fullpath) - return config, nil -} diff --git a/robot/config_test.go b/robot/config_test.go deleted file mode 100644 index 9fecbd6d..00000000 --- a/robot/config_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package robot_test - -import ( - "strings" - "testing" - - "github.com/robocorp/rcc/hamlet" - "github.com/robocorp/rcc/robot" -) - -const ( - minimalActivity = ` -activities: - Main activity: - output: output - activityRoot: .` -) - -func TestCannotReadMissingActivityConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - sut, err := robot.LoadYamlConfiguration("testdata/badmissing.yaml") - wont_be.Nil(err) - must_be.Nil(sut) - - sut, err = robot.LoadActivityPackage("testdata/bad.yaml") - wont_be.Nil(err) - must_be.Nil(sut) -} - -func TestCanReadTemplateActivityConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - raw, err := robot.LoadActivityPackage("testdata/template.yaml") - must_be.Nil(err) - wont_be.Nil(raw) - sut := raw.(*robot.Config) - must_be.Equal("config/conda.yaml", sut.Conda) - must_be.True(strings.HasSuffix(sut.CondaConfigFile(), "config/conda.yaml")) - must_be.Equal(1, len(sut.Activities)) - activity := sut.DefaultTask().(*robot.Activity) - wont_be.Nil(activity) - must_be.True(strings.HasSuffix(activity.WorkingDirectory(sut), "/testdata")) - must_be.Nil(sut.TaskByName("Missing Activity Name")) - must_be.Same(activity, sut.TaskByName("My activity")) - must_be.Same(activity, sut.TaskByName("my Activity")) - must_be.Equal([]string{"My activity"}, sut.AvailableTasks()) - must_be.True(strings.HasSuffix(activity.ArtifactDirectory(sut), "output")) - must_be.Equal(".", activity.Root) - wont_be.Nil(activity.Environment) - wont_be.Nil(activity.Action) - must_be.Equal(10, len(activity.Commandline())) - must_be.Equal(1, len(activity.Paths(sut))) - must_be.True(len(activity.ExecutionEnvironment(sut, "foobar", []string{}, false)) > 3) - must_be.True(len(activity.ExecutionEnvironment(sut, "foobar", []string{}, true)) > len(activity.ExecutionEnvironment(sut, "foobar", []string{}, false))) - must_be.Equal(3, len(activity.PythonPaths(sut))) - ok, err := sut.Validate() - must_be.True(ok) - must_be.Nil(err) -} - -func TestCanReadComplexActivityConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - raw, err := robot.LoadActivityPackage("testdata/complex.yaml") - must_be.Nil(err) - wont_be.Nil(raw) - sut := raw.(*robot.Config) - wont_be.Nil(sut.IgnoreFiles()) - must_be.Equal(2, len(sut.IgnoreFiles())) - must_be.Nil(sut.DefaultTask()) - must_be.Equal("conda.yaml", sut.Conda) - must_be.Equal(2, len(sut.Activities)) - wont_be.Nil(sut.TaskByName("Read Excel to work item")) - wont_be.Nil(sut.TaskByName("Generate PDFs from work item")) - must_be.Equal([]string{"Generate PDFs from work item", "Read Excel to work item"}, sut.AvailableTasks()) - ok, err := sut.Validate() - must_be.True(ok) - must_be.Nil(err) -} - -func TestCanReadEnvironmentlessActivityConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - raw, err := robot.LoadActivityPackage("testdata/complex.yaml") - must_be.Nil(err) - wont_be.Nil(raw) - sut := raw.(*robot.Config) - activity := sut.DefaultTask() - must_be.Nil(activity) - ok, err := sut.Validate() - must_be.True(ok) - must_be.Nil(err) -} - -func TestCanReadActivityConfigFromText(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - sut, err := robot.ActivityPackageFrom([]byte("")) - must_be.Nil(err) - wont_be.Nil(sut) - must_be.Nil(sut.DefaultTask()) - must_be.Nil(sut.TaskByName("")) - must_be.Nil(sut.TaskByName("foo")) - must_be.Equal("", sut.CondaConfigFile()) - must_be.Equal("", sut.CondaConfigFile()) - ok, err := sut.Validate() - wont_be.True(ok) - wont_be.Nil(err) -} - -func TestCanParseMinimalActivityConfig(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - sut, err := robot.ActivityPackageFrom([]byte(minimalActivity)) - must_be.Nil(err) - wont_be.Nil(sut) - wont_be.Nil(sut.DefaultTask()) - wont_be.Nil(sut.TaskByName("")) - must_be.Nil(sut.TaskByName("foo")) - must_be.Equal("", sut.CondaConfigFile()) - must_be.Equal("", sut.CondaConfigFile()) - ok, err := sut.Validate() - wont_be.True(ok) - wont_be.Nil(err) -} - -func TestCanReadActivityConfigFromBadText(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - sut, err := robot.ActivityPackageFrom([]byte(":")) - wont_be.Nil(err) - must_be.Nil(sut) -} diff --git a/robot/robot.go b/robot/robot.go index cd732f92..c4eddd59 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -12,7 +12,6 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/pretty" "github.com/google/shlex" "gopkg.in/yaml.v2" @@ -28,13 +27,12 @@ type Robot interface { RootDirectory() string Validate() (bool, error) - // compatibility "string" argument (task name) - WorkingDirectory(string) string - ArtifactDirectory(string) string - Paths(string) pathlib.PathParts - PythonPaths(string) pathlib.PathParts - SearchPath(taskname, location string) pathlib.PathParts - ExecutionEnvironment(taskname, location string, inject []string, full bool) []string + WorkingDirectory() string + ArtifactDirectory() string + Paths() pathlib.PathParts + PythonPaths() pathlib.PathParts + SearchPath(location string) pathlib.PathParts + ExecutionEnvironment(location string, inject []string, full bool) []string } type Task interface { @@ -153,11 +151,11 @@ func (it *robot) CondaConfigFile() string { return filepath.Join(it.Root, it.Conda) } -func (it *robot) WorkingDirectory(string) string { +func (it *robot) WorkingDirectory() string { return it.Root } -func (it *robot) ArtifactDirectory(string) string { +func (it *robot) ArtifactDirectory() string { return filepath.Join(it.Root, it.Artifacts) } @@ -177,31 +175,31 @@ func pathBuilder(root string, tails []string) pathlib.PathParts { return pathlib.PathFrom(result...) } -func (it *robot) Paths(string) pathlib.PathParts { +func (it *robot) Paths() pathlib.PathParts { if it == nil { return pathlib.PathFrom() } return pathBuilder(it.Root, it.Path) } -func (it *robot) PythonPaths(string) pathlib.PathParts { +func (it *robot) PythonPaths() pathlib.PathParts { if it == nil { return pathlib.PathFrom() } return pathBuilder(it.Root, it.Pythonpath) } -func (it *robot) SearchPath(taskname, location string) pathlib.PathParts { - return conda.FindPath(location).Prepend(it.Paths("")...) +func (it *robot) SearchPath(location string) pathlib.PathParts { + return conda.FindPath(location).Prepend(it.Paths()...) } -func (it *robot) ExecutionEnvironment(taskname, location string, inject []string, full bool) []string { +func (it *robot) ExecutionEnvironment(location string, inject []string, full bool) []string { environment := make([]string, 0, 100) if full { environment = append(environment, os.Environ()...) } environment = append(environment, inject...) - searchPath := it.SearchPath(taskname, location) + searchPath := it.SearchPath(location) python, ok := searchPath.Which("python3", conda.FileExtensions) if !ok { python, ok = searchPath.Which("python", conda.FileExtensions) @@ -223,34 +221,34 @@ func (it *robot) ExecutionEnvironment(taskname, location string, inject []string "TEMP="+conda.RobocorpTemp(), "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), - it.PythonPaths("").AsEnvironmental("PYTHONPATH"), - fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory("")), - fmt.Sprintf("ROBOT_ARTIFACTS=%s", it.ArtifactDirectory("")), + it.PythonPaths().AsEnvironmental("PYTHONPATH"), + fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory()), + fmt.Sprintf("ROBOT_ARTIFACTS=%s", it.ArtifactDirectory()), ) } func (it *task) WorkingDirectory(robot Robot) string { - return robot.WorkingDirectory("") + return robot.WorkingDirectory() } func (it *task) ArtifactDirectory(robot Robot) string { - return robot.ArtifactDirectory("") + return robot.ArtifactDirectory() } func (it *task) SearchPath(robot Robot, location string) pathlib.PathParts { - return robot.SearchPath("", location) + return robot.SearchPath(location) } func (it *task) Paths(robot Robot) pathlib.PathParts { - return robot.Paths("") + return robot.Paths() } func (it *task) PythonPaths(robot Robot) pathlib.PathParts { - return robot.PythonPaths("") + return robot.PythonPaths() } func (it *task) ExecutionEnvironment(robot Robot, location string, inject []string, full bool) []string { - return robot.ExecutionEnvironment("", location, inject, full) + return robot.ExecutionEnvironment(location, inject, full) } func (it *task) shellCommand() []string { @@ -293,6 +291,15 @@ func robotFrom(content []byte) (*robot, error) { return &config, nil } +func PlainEnvironment(inject []string, full bool) []string { + environment := make([]string, 0, 100) + if full { + environment = append(environment, os.Environ()...) + } + environment = append(environment, inject...) + return environment +} + func LoadRobotYaml(filename string) (Robot, error) { fullpath, err := filepath.Abs(filename) if err != nil { @@ -310,22 +317,10 @@ func LoadRobotYaml(filename string) (Robot, error) { return robot, nil } -func LoadYamlConfiguration(filename string) (Robot, error) { - if strings.HasSuffix(filename, "package.yaml") { - common.Log("%sWARNING! Support for 'package.yaml' is deprecated. Upgrade to 'robot.yaml'!%s", pretty.Red, pretty.Reset) - return LoadActivityPackage(filename) - } - return LoadRobotYaml(filename) -} - func DetectConfigurationName(directory string) string { robot, err := pathlib.FindNamedPath(directory, "robot.yaml") if err == nil && len(robot) > 0 { return robot } - robot, err = pathlib.FindNamedPath(directory, "package.yaml") - if err == nil && len(robot) > 0 { - return robot - } - return filepath.Join(directory, "package.yaml") + return filepath.Join(directory, "robot.yaml") } diff --git a/robot/robot_test.go b/robot/robot_test.go index 3056abd3..32bb1ce5 100644 --- a/robot/robot_test.go +++ b/robot/robot_test.go @@ -30,12 +30,12 @@ func TestCanReadRealRobotYaml(t *testing.T) { wont.Nil(sut.TaskByName("task form name")) wont.Nil(sut.TaskByName("Shell Form Name")) wont.Nil(sut.TaskByName(" Old command form name ")) - must.Equal(1, len(sut.Paths(""))) - must.Equal(3, len(sut.PythonPaths(""))) - must.True(2 < len(sut.SearchPath("", "."))) + must.Equal(1, len(sut.Paths())) + must.Equal(3, len(sut.PythonPaths())) + must.True(2 < len(sut.SearchPath("."))) must.True(strings.HasSuffix(sut.CondaConfigFile(), "conda.yaml")) - must.True(strings.HasSuffix(sut.WorkingDirectory(""), "testdata")) - must.True(strings.HasSuffix(sut.ArtifactDirectory(""), "output")) + must.True(strings.HasSuffix(sut.WorkingDirectory(), "testdata")) + must.True(strings.HasSuffix(sut.ArtifactDirectory(), "output")) valid, err := sut.Validate() must.True(valid) must.Nil(err) From 7166df68833872e4f43b4baacee746dd176d83ff Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 21 Jan 2021 08:48:12 +0200 Subject: [PATCH 049/516] RCC-126: environment leasing (v9.0.2) - fix: prevent direct deletion of leased environment --- cmd/delete.go | 6 +++++- common/version.go | 2 +- conda/workflows.go | 6 +++++- docs/changelog.md | 4 ++++ robot_tests/lease.robot | 5 +++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/delete.go b/cmd/delete.go index 989640fd..64bf0c2b 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -15,7 +16,10 @@ After deletion, it will not be available anymore.`, Run: func(cmd *cobra.Command, args []string) { for _, label := range args { common.Log("Removing %v", label) - conda.RemoveEnvironment(label) + err := conda.RemoveEnvironment(label) + if err != nil { + pretty.Exit(1, "Error: %v", err) + } } }, } diff --git a/common/version.go b/common/version.go index d6846891..cea42156 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.0.1` + Version = `v9.0.2` ) diff --git a/conda/workflows.go b/conda/workflows.go index e437dcb1..86af631b 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -345,9 +345,13 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return "", errors.New("Could not create environment.") } -func RemoveEnvironment(label string) { +func RemoveEnvironment(label string) error { + if IsLeasedEnvironment(label) { + return fmt.Errorf("WARNING: %q is leased by %q and wont be deleted!", label, WhoLeased(label)) + } removeClone(LiveFrom(label)) removeClone(TemplateFrom(label)) + return nil } func removeClone(location string) { diff --git a/docs/changelog.md b/docs/changelog.md index 66da68c3..9207e52c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.0.2 (date: 21.1.2021) + +- fix: prevent direct deletion of leased environment + ## v9.0.1 (date: 20.1.2021) - BREAKING CHANGES diff --git a/robot_tests/lease.robot b/robot_tests/lease.robot index 4a7c1fd9..e6b3160a 100644 --- a/robot_tests/lease.robot +++ b/robot_tests/lease.robot @@ -48,8 +48,13 @@ Can operate leased environments Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef 1 Must Have Error: + Goal Cannot delete someone elses leased environment + Step build/rcc env delete 8f1d3dc95228edef 1 + Must Have WARNING: "8f1d3dc95228edef" is leased by "taker (1)" and wont be deleted! + Goal Check listing for taker information (still same) Step build/rcc env list + Must Have 8f1d3dc95228edef Must Have "taker (1)" Wont Have "second (2)" From 27258a2e009fd9bf19953d44fece37f85db8d756 Mon Sep 17 00:00:00 2001 From: Antti Karjalainen Date: Sun, 24 Jan 2021 22:07:31 +0200 Subject: [PATCH 050/516] Add CLI install instructions to readme (#9) --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e187301c..e6100eea 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) co ## Getting Started :arrow_double_down: Install rcc -> [Download RCC](#direct-downloads-for-signed-executables-provided-by-robocorp) +> [Install](#installing-rcc-from-command-line) or [Download RCC](#direct-downloads-for-signed-executables-provided-by-robocorp) :octocat: Pull robot from GitHub: > `rcc pull github.com/robocorp/example-google-image-search` @@ -25,6 +25,40 @@ Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) co For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.com/docs/product-manuals/robocorp-cli) to get started. To build `rcc` from this repository see the [Setup Guide](/docs/BUILD.md) +## Installing RCC from command line + +### Windows + +1. Open the command prompt +1. Download: `curl -o rcc.exe https://downloads.code.robocorp.com/rcc/latest/windows64/rcc.exe` +1. [Add to system path](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/): Open Start -> `Edit the system environment variables` +1. Test: `rcc` + +### macOS + +#### Brew cask from Robocorp tap + +1. Install: `brew install robocorp/tools/rcc` +1. Test: `rcc` + +Upgrading: `brew upgrade rcc` + +#### Raw download + +1. Open the terminal +1. Download: `curl -o rcc https://downloads.code.robocorp.com/rcc/latest/macos64/rcc` +1. Make the downloaded file executable: `chmod a+x rcc` +1. Add to path: `sudo mv rcc /usr/local/bin/` +1. Test: `rcc` + +### Linux + +1. Open the terminal +1. Download: `curl -o rcc https://downloads.code.robocorp.com/rcc/latest/linux64/rcc` +1. Make the downloaded file executable: `chmod a+x rcc` +1. Add to path: `sudo mv rcc /usr/local/bin/` +1. Test: `rcc` + ### Direct downloads for signed executables provided by Robocorp | OS | Download URL | From 1c053450177907f910866e9812b5f4088bda9288 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 25 Jan 2021 14:25:49 +0200 Subject: [PATCH 051/516] RCC-105: diagnostics command (v9.1.0) - new command `rcc configure diagnostics` to help identify environment related issues - also requiring new version of micromamba, 0.7.10 --- cmd/diagnostics.go | 61 +++++++++++++++ common/version.go | 2 +- conda/robocorp.go | 17 ++-- docs/changelog.md | 6 ++ operations/diagnostics.go | 151 ++++++++++++++++++++++++++++++++++++ robot_tests/exitcodes.robot | 2 + robot_tests/fullrun.robot | 3 + 7 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 cmd/diagnostics.go create mode 100644 operations/diagnostics.go diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go new file mode 100644 index 00000000..4ad22107 --- /dev/null +++ b/cmd/diagnostics.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + "sort" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +func jsonDiagnostics(details *operations.DiagnosticStatus) { + form, err := details.AsJson() + if err != nil { + pretty.Exit(1, "Error: %s", err) + } + fmt.Println(form) +} + +func humaneDiagnostics(details *operations.DiagnosticStatus) { + common.Log("Diagnostics:") + keys := make([]string, 0, len(details.Details)) + for key, _ := range details.Details { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + value := details.Details[key] + common.Log(" - %-18s... %q", key, value) + } + common.Log("") + common.Log("Checks:") + for _, check := range details.Checks { + common.Log(" - %-8s %-8s %s", check.Type, check.Status, check.Message) + } +} + +var diagnosticsCmd = &cobra.Command{ + Use: "diagnostics", + Short: "Run system diagnostics to help resolve rcc issues.", + Long: "Run system diagnostics to help resolve rcc issues.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Diagnostic run lasted").Report() + } + result := operations.RunDiagnostics() + if jsonFlag { + jsonDiagnostics(result) + } else { + humaneDiagnostics(result) + pretty.Ok() + } + }, +} + +func init() { + configureCmd.AddCommand(diagnosticsCmd) + diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") +} diff --git a/common/version.go b/common/version.go index cea42156..11f4215e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.0.2` + Version = `v9.1.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 9afd7a5a..e8b677f4 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -208,16 +208,21 @@ func asVersion(text string) (uint64, string) { return version, text } -func HasMicroMamba() bool { - if !pathlib.IsFile(BinMicromamba()) { - return false - } +func MicromambaVersion() string { versionText, _, err := shell.New(CondaEnvironment(), ".", BinMicromamba(), "--version").CaptureOutput() if err != nil { + return err.Error() + } + _, versionText = asVersion(versionText) + return versionText +} + +func HasMicroMamba() bool { + if !pathlib.IsFile(BinMicromamba()) { return false } - version, versionText := asVersion(versionText) - goodEnough := version >= 7008 + version, versionText := asVersion(MicromambaVersion()) + goodEnough := version >= 7010 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/docs/changelog.md b/docs/changelog.md index 9207e52c..029382f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.1.0 (date: 25.1.2021) + +- new command `rcc configure diagnostics` to help identify environment + related issues +- also requiring new version of micromamba, 0.7.10 + ## v9.0.2 (date: 21.1.2021) - fix: prevent direct deletion of leased environment diff --git a/operations/diagnostics.go b/operations/diagnostics.go new file mode 100644 index 00000000..88c2f3b5 --- /dev/null +++ b/operations/diagnostics.go @@ -0,0 +1,151 @@ +package operations + +import ( + "encoding/json" + "fmt" + "net" + "os" + "runtime" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/xviper" +) + +const ( + canaryHost = `https://downloads.robocorp.com` + canaryUrl = `/canary.txt` +) + +var ( + checkedHosts = []string{ + `api.eu1.robocloud.eu`, + `downloads.robocorp.com`, + `pypi.org`, + `conda.anaconda.org`, + `github.com`, + `files.pythonhosted.org`, + } +) + +type DiagnosticStatus struct { + Details map[string]string `json:"details"` + Checks []*DiagnosticCheck `json:"checks"` +} + +type DiagnosticCheck struct { + Type string `json:"type"` + Status string `json:"status"` + Message string `json:"message"` +} + +func (it *DiagnosticStatus) AsJson() (string, error) { + body, err := json.MarshalIndent(it, "", " ") + if err != nil { + return "", err + } + return string(body), nil +} + +type stringerr func() (string, error) + +func justText(source stringerr) string { + result, _ := source() + return result +} + +func RunDiagnostics() *DiagnosticStatus { + result := &DiagnosticStatus{ + Details: make(map[string]string), + Checks: []*DiagnosticCheck{}, + } + executable, _ := os.Executable() + result.Details["executable"] = executable + result.Details["rcc"] = common.Version + result.Details["stats"] = rccStatusLine() + result.Details["micromamba"] = conda.MicromambaVersion() + result.Details["ROBOCORP_HOME"] = conda.RobocorpHome() + result.Details["user-cache-dir"] = justText(os.UserCacheDir) + result.Details["user-config-dir"] = justText(os.UserConfigDir) + result.Details["user-home-dir"] = justText(os.UserHomeDir) + //result.Details["hostname"] = justText(os.Hostname) + result.Details["tempdir"] = os.TempDir() + result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) + result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) + + // checks + result.Checks = append(result.Checks, longPathSupportCheck()) + for _, host := range checkedHosts { + result.Checks = append(result.Checks, dnsLookupCheck(host)) + } + result.Checks = append(result.Checks, canaryDownloadCheck()) + return result +} + +func rccStatusLine() string { + requests := xviper.GetInt("stats.env.request") + hits := xviper.GetInt("stats.env.hit") + dirty := xviper.GetInt("stats.env.dirty") + misses := xviper.GetInt("stats.env.miss") + failures := xviper.GetInt("stats.env.failures") + merges := xviper.GetInt("stats.env.merges") + templates := len(conda.TemplateList()) + return fmt.Sprintf("%d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s", templates, requests, merges, hits, dirty, misses, failures, xviper.TrackingIdentity()) +} + +func longPathSupportCheck() *DiagnosticCheck { + if conda.HasLongPathSupport() { + return &DiagnosticCheck{ + Type: "OS", + Status: "ok", + Message: "Supports long enough paths.", + } + } + return &DiagnosticCheck{ + Type: "OS", + Status: "fail", + Message: "Does not support long path names!", + } +} + +func dnsLookupCheck(site string) *DiagnosticCheck { + found, err := net.LookupHost(site) + if err != nil { + return &DiagnosticCheck{ + Type: "network", + Status: "fail", + Message: fmt.Sprintf("DNS lookup %s failed: %v", site, err), + } + } + return &DiagnosticCheck{ + Type: "network", + Status: "ok", + Message: fmt.Sprintf("%s found: %v", site, found), + } +} + +func canaryDownloadCheck() *DiagnosticCheck { + client, err := cloud.NewClient(canaryHost) + if err != nil { + return &DiagnosticCheck{ + Type: "network", + Status: "fail", + Message: fmt.Sprintf("%v: %v", canaryHost, err), + } + } + request := client.NewRequest(canaryUrl) + response := client.Get(request) + if response.Status != 200 || string(response.Body) != "Used to testing connections" { + return &DiagnosticCheck{ + Type: "network", + Status: "fail", + Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), + } + } + return &DiagnosticCheck{ + Type: "network", + Status: "ok", + Message: fmt.Sprintf("Canary download successful: %s%s", canaryHost, canaryUrl), + } +} diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index e01fca93..0d083fb4 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -42,6 +42,8 @@ Help for rcc env new 0 build/rcc env new -h --controller cites Help for rcc env variables 0 build/rcc env variables -h --controller citests Help for rcc configure identity 0 build/rcc configure identity -h --controller citests +Help for rcc configure diagnostics 0 build/rcc configure diagnostics -h --controller citests + Help for rcc feedback metric 0 build/rcc feedback metric -h --controller citests Help for rcc man license 0 build/rcc man license -h --controller citests diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6da51cd9..6f65483e 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -151,3 +151,6 @@ Using and running template example with shell file Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Be Json Response + Goal See diagnostics as valid JSON form + Step build/rcc configure diagnostics --json + Must Be Json Response From 859c927e25225c5f694b27703b094b0e02ca2de9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Jan 2021 12:29:10 +0200 Subject: [PATCH 052/516] EXPERIMENT: carrier tryout (v9.2.0) --- cmd/carrier.go | 107 ++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 + operations/carrier.go | 173 ++++++++++++++++++++++++++++++++ operations/carrier_test.go | 40 ++++++++ operations/testdata/payload.txt | 1 + operations/zipper.go | 55 +++++++++- 7 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 cmd/carrier.go create mode 100644 operations/carrier.go create mode 100644 operations/carrier_test.go create mode 100644 operations/testdata/payload.txt diff --git a/cmd/carrier.go b/cmd/carrier.go new file mode 100644 index 00000000..05a169e2 --- /dev/null +++ b/cmd/carrier.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" + "github.com/robocorp/rcc/xviper" + + "github.com/spf13/cobra" +) + +var ( + carrierFile string + carrierBuild bool + carrierRun bool +) + +func buildCarrier() error { + err := operations.SelfCopy(carrierFile) + if err != nil { + return err + } + return operations.SelfAppend(carrierFile, zipfile) +} + +func runCarrier() error { + ok, err := operations.IsCarrier() + if err != nil { + return err + } + if !ok { + return fmt.Errorf("This executable is not carrier!") + } + if common.DebugFlag { + defer common.Stopwatch("Task testrun lasted").Report() + } + ok = conda.MustMicromamba() + if !ok { + pretty.Exit(4, "Could not get micromamba installed.") + } + defer xviper.RunMinutes().Done() + now := time.Now() + marker := now.Unix() + testrunDir := filepath.Join(".", now.Format("2006-01-02_15_04_05")) + err = os.MkdirAll(testrunDir, 0o755) + if err != nil { + return err + } + sentinelTime := time.Now() + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", marker)) + defer os.RemoveAll(workarea) + common.Debug("Using temporary workarea: %v", workarea) + carrier, err := operations.FindExecutable() + if err != nil { + return err + } + err = operations.CarrierUnzip(workarea, carrier, false, true) + if err != nil { + return err + } + defer pathlib.Walk(workarea, pathlib.IgnoreOlder(sentinelTime).Ignore, TargetDir(testrunDir).CopyBack) + targetRobot := robot.DetectConfigurationName(workarea) + simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) + defer common.Log("Moving outputs to %v directory.", testrunDir) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) + operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) + return nil +} + +var carrierCmd = &cobra.Command{ + Use: "carrier", + Short: "Create carrier rcc with payload.", + Long: "Create carrier rcc with payload.", + Run: func(cmd *cobra.Command, args []string) { + defer common.Stopwatch("rcc carrier lasted").Report() + if carrierBuild { + err := buildCarrier() + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + pretty.Ok() + } + if carrierRun { + err := runCarrier() + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + } + }, +} + +func init() { + internalCmd.AddCommand(carrierCmd) + carrierCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the carrier payload.") + carrierCmd.Flags().StringVarP(&carrierFile, "carrier", "c", "carrier.exe", "The filename for the resulting carrier executable.") + carrierCmd.Flags().BoolVarP(&carrierBuild, "build", "b", false, "Build actual carrier executable.") + carrierCmd.Flags().BoolVarP(&carrierRun, "run", "r", false, "Run this executable as robot carrier.") +} diff --git a/common/version.go b/common/version.go index 11f4215e..f6e7f0a6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.1.0` + Version = `v9.2.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 029382f2..4a3bf458 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.2.0 (date: 25.1.2021) + +- experiment: carrier PoC + ## v9.1.0 (date: 25.1.2021) - new command `rcc configure diagnostics` to help identify environment diff --git a/operations/carrier.go b/operations/carrier.go new file mode 100644 index 00000000..6319fdb5 --- /dev/null +++ b/operations/carrier.go @@ -0,0 +1,173 @@ +package operations + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" +) + +const ( + scissors = `---8x---` +) + +func FindExecutable() (string, error) { + self, err := os.Executable() + if err != nil { + return "", err + } + self, err = filepath.EvalSymlinks(self) + if err != nil { + return "", err + } + self, err = filepath.Abs(self) + if err != nil { + return "", err + } + return self, nil +} + +func SelfCopy(target string) error { + self, err := FindExecutable() + if err != nil { + return err + } + source, err := os.Open(self) + if err != nil { + return err + } + defer source.Close() + + sink, err := os.OpenFile(target, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o755) + if err != nil { + return err + } + defer sink.Close() + size, err := io.Copy(sink, source) + if err != nil { + return err + } + common.Debug("Copied %q to %q as size of %d bytes.", self, target, size) + return nil +} + +func SelfAppend(target, payload string) error { + size, ok := pathlib.Size(target) + if !ok { + return fmt.Errorf("Could not get size of %q.", target) + } + source, err := os.Open(payload) + if err != nil { + return err + } + defer source.Close() + sink, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0o755) + if err != nil { + return err + } + defer sink.Close() + _, err = sink.Write([]byte(scissors)) + if err != nil { + return err + } + _, err = io.Copy(sink, source) + if err != nil { + return err + } + err = binary.Write(sink, binary.LittleEndian, size) + if err != nil { + return err + } + return nil +} + +func HasPayload(filename string) (bool, error) { + reader, err := PayloadReaderAt(filename) + if err != nil { + return false, err + } + reader.Close() + return true, nil +} + +func IsCarrier() (bool, error) { + carrier, err := FindExecutable() + if err != nil { + return false, err + } + return HasPayload(carrier) +} + +type ReaderCloserAt interface { + io.ReaderAt + io.Closer + Limit() int64 +} + +type carrier struct { + source *os.File + offset int64 + limit int64 +} + +func (it *carrier) Limit() int64 { + return it.limit +} + +func (it *carrier) ReadAt(target []byte, offset int64) (int, error) { + _, err := it.source.Seek(it.offset+offset, 0) + if err != nil { + return 0, err + } + return it.source.Read(target) +} + +func (it *carrier) Close() error { + return it.source.Close() +} + +func PayloadReaderAt(filename string) (ReaderCloserAt, error) { + size, ok := pathlib.Size(filename) + if !ok { + return nil, fmt.Errorf("Could not get size of %q.", filename) + } + source, err := os.Open(filename) + if err != nil { + return nil, err + } + _, err = source.Seek(size-8, 0) + if err != nil { + source.Close() + return nil, err + } + var offset int64 + err = binary.Read(source, binary.LittleEndian, &offset) + if err != nil { + source.Close() + return nil, err + } + if offset < 0 || size <= offset { + source.Close() + return nil, fmt.Errorf("%q has no carrier payload.", filename) + } + _, err = source.Seek(offset, 0) + if err != nil { + source.Close() + return nil, err + } + marker := make([]byte, 8) + count, err := source.Read(marker) + if err != nil { + source.Close() + return nil, err + } + if count != 8 || string(marker) != scissors { + source.Close() + return nil, fmt.Errorf("%q has no carrier payload.", filename) + } + return &carrier{source, offset + 8, size - offset - 16}, nil +} diff --git a/operations/carrier_test.go b/operations/carrier_test.go new file mode 100644 index 00000000..63676d6e --- /dev/null +++ b/operations/carrier_test.go @@ -0,0 +1,40 @@ +package operations_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" +) + +func TestCanUseCarrier(t *testing.T) { + must, wont := hamlet.Specifications(t) + + tempFile := filepath.Join(os.TempDir(), "carrier") + if pathlib.Exists(tempFile) { + os.Remove(tempFile) + } + + wont.True(pathlib.Exists(tempFile)) + must.Nil(operations.SelfCopy(tempFile)) + must.True(pathlib.Exists(tempFile)) + must.Nil(operations.SelfCopy(tempFile)) + must.True(pathlib.Exists(tempFile)) + + original, ok := pathlib.Size(tempFile) + must.True(ok) + + must.Nil(operations.SelfAppend(tempFile, "testdata/payload.txt")) + + final, ok := pathlib.Size(tempFile) + must.True(ok) + + must.Equal(original+24, final) + + ok, err := operations.HasPayload(tempFile) + must.Nil(err) + must.True(ok) +} diff --git a/operations/testdata/payload.txt b/operations/testdata/payload.txt new file mode 100644 index 00000000..c2981a99 --- /dev/null +++ b/operations/testdata/payload.txt @@ -0,0 +1 @@ +payload diff --git a/operations/zipper.go b/operations/zipper.go index 358b70fa..860e8dee 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -49,11 +49,27 @@ func (it *WriteTarget) Execute() bool { } type unzipper struct { - reader *zip.ReadCloser + reader *zip.Reader + closer io.Closer } func (it *unzipper) Close() { - it.reader.Close() + it.closer.Close() +} + +func newPayloadUnzipper(filename string) (*unzipper, error) { + payloader, err := PayloadReaderAt(filename) + if err != nil { + return nil, err + } + reader, err := zip.NewReader(payloader, payloader.Limit()) + if err != nil { + return nil, err + } + return &unzipper{ + reader: reader, + closer: payloader, + }, nil } func newUnzipper(filename string) (*unzipper, error) { @@ -62,7 +78,8 @@ func newUnzipper(filename string) (*unzipper, error) { return nil, err } return &unzipper{ - reader: reader, + reader: &reader.Reader, + closer: reader, }, nil } @@ -198,6 +215,38 @@ func defaultIgnores(selfie string) pathlib.Ignore { return pathlib.CompositeIgnore(result...) } +func CarrierUnzip(directory, carrier string, force, temporary bool) error { + fullpath, err := filepath.Abs(directory) + if err != nil { + return err + } + if force { + err = pathlib.EnsureDirectoryExists(fullpath) + } else { + err = pathlib.EnsureEmptyDirectory(fullpath) + } + if err != nil { + return err + } + unzip, err := newPayloadUnzipper(carrier) + if err != nil { + return err + } + defer unzip.Close() + err = unzip.Extract(fullpath) + if err != nil { + return err + } + if temporary { + return nil + } + err = UpdateRobot(fullpath) + if err != nil { + return err + } + return FixDirectory(fullpath) +} + func Unzip(directory, zipfile string, force, temporary bool) error { fullpath, err := filepath.Abs(directory) if err != nil { From 3d80ae9e3dd28ae79ba8a3619f9a94dd699404cf Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Mon, 25 Jan 2021 17:32:18 +0200 Subject: [PATCH 053/516] Updated the download links in readme.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e6100eea..39d0a5ce 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.c ### Windows 1. Open the command prompt -1. Download: `curl -o rcc.exe https://downloads.code.robocorp.com/rcc/latest/windows64/rcc.exe` +1. Download: `curl -o rcc.exe https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe` 1. [Add to system path](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/): Open Start -> `Edit the system environment variables` 1. Test: `rcc` @@ -46,7 +46,7 @@ Upgrading: `brew upgrade rcc` #### Raw download 1. Open the terminal -1. Download: `curl -o rcc https://downloads.code.robocorp.com/rcc/latest/macos64/rcc` +1. Download: `curl -o rcc https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc` 1. Make the downloaded file executable: `chmod a+x rcc` 1. Add to path: `sudo mv rcc /usr/local/bin/` 1. Test: `rcc` @@ -54,18 +54,18 @@ Upgrading: `brew upgrade rcc` ### Linux 1. Open the terminal -1. Download: `curl -o rcc https://downloads.code.robocorp.com/rcc/latest/linux64/rcc` +1. Download: `curl -o rcc https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc` 1. Make the downloaded file executable: `chmod a+x rcc` 1. Add to path: `sudo mv rcc /usr/local/bin/` 1. Test: `rcc` ### Direct downloads for signed executables provided by Robocorp -| OS | Download URL | -| ------- | ---------------------------------------------------------------- | -| Windows | https://downloads.code.robocorp.com/rcc/latest/windows64/rcc.exe | -| macOS | https://downloads.code.robocorp.com/rcc/latest/macos64/rcc | -| Linux | https://downloads.code.robocorp.com/rcc/latest/linux64/rcc | +| OS | Download URL | +| ------- | -------------------------------------------------------------------- | +| Windows | https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe | +| macOS | https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc | +| Linux | https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc | *[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* @@ -81,4 +81,4 @@ You can also use the [Robocorp Forum](https://forum.robocorp.com) ## License -Apache 2.0 \ No newline at end of file +Apache 2.0 From dc5638d82a04c260d099ff6608bc5368e2d0c0eb Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Mon, 25 Jan 2021 18:55:47 +0200 Subject: [PATCH 054/516] Added missing v8.0.12 to changelog --- docs/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 4a3bf458..2b2da9a2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -31,6 +31,9 @@ with exactly same specification (if pristine, it is shared, otherwise it is an error) +## v8.0.12 (date: 18.1.2021) +- Templates conda -channel ordering reverted pending conda-forge chagnes. + ## v8.0.10 (date: 18.1.2021) - fix: when there is no pip dependencies, do not try to run pip command From c8115804f6b4352df6164fa6f3434b03d14357de Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 28 Jan 2021 14:45:33 +0200 Subject: [PATCH 055/516] RCC-132: submit issue report via rcc (v9.3.0) - support for applications to submit issue reports thru rcc - print "robot.yaml" to logs, to make it visible for support cases - diagnostics can now print into a file, and that is used as part of issue reporting - added links to diagnostic checks, for user guidance --- cmd/diagnostics.go | 42 +++-------- cmd/issue.go | 36 ++++++++++ cmd/variables.go | 2 +- common/version.go | 2 +- conda/validate.go | 6 +- docs/changelog.md | 8 +++ operations/assistant.go | 4 +- operations/authorize.go | 8 +++ operations/diagnostics.go | 110 ++++++++++++++++++++++++++--- operations/fixing.go | 2 +- operations/issues.go | 135 ++++++++++++++++++++++++++++++++++++ operations/running.go | 2 +- operations/zipper.go | 8 ++- robot/robot.go | 5 +- robot/robot_test.go | 8 +-- robot_tests/fullrun.robot | 14 +++- robot_tests/lease.robot | 14 +++- robot_tests/resources.robot | 15 +++- robot_tests/supporting.py | 6 ++ 19 files changed, 361 insertions(+), 66 deletions(-) create mode 100644 cmd/issue.go create mode 100644 operations/issues.go diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 4ad22107..b82b05c1 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -1,9 +1,6 @@ package cmd import ( - "fmt" - "sort" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" @@ -11,31 +8,9 @@ import ( "github.com/spf13/cobra" ) -func jsonDiagnostics(details *operations.DiagnosticStatus) { - form, err := details.AsJson() - if err != nil { - pretty.Exit(1, "Error: %s", err) - } - fmt.Println(form) -} - -func humaneDiagnostics(details *operations.DiagnosticStatus) { - common.Log("Diagnostics:") - keys := make([]string, 0, len(details.Details)) - for key, _ := range details.Details { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - value := details.Details[key] - common.Log(" - %-18s... %q", key, value) - } - common.Log("") - common.Log("Checks:") - for _, check := range details.Checks { - common.Log(" - %-8s %-8s %s", check.Type, check.Status, check.Message) - } -} +var ( + fileOption string +) var diagnosticsCmd = &cobra.Command{ Use: "diagnostics", @@ -45,17 +20,16 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - result := operations.RunDiagnostics() - if jsonFlag { - jsonDiagnostics(result) - } else { - humaneDiagnostics(result) - pretty.Ok() + err := operations.PrintDiagnostics(fileOption, jsonFlag) + if err != nil { + pretty.Exit(1, "Error: %v", err) } + pretty.Ok() }, } func init() { configureCmd.AddCommand(diagnosticsCmd) diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + diagnosticsCmd.Flags().StringVarP(&fileOption, "file", "f", "", "Save output into a file.") } diff --git a/cmd/issue.go b/cmd/issue.go new file mode 100644 index 00000000..51b3a4bb --- /dev/null +++ b/cmd/issue.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + issueMetafile string + issueAttachments []string +) + +var issueCmd = &cobra.Command{ + Use: "issue", + Short: "Send an issue to Robocorp Cloud via rcc.", + Long: "Send an issue to Robocorp Cloud via rcc.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Feedback issue lasted").Report() + } + err := operations.ReportIssue(issueMetafile, issueAttachments) + if err != nil { + pretty.Exit(1, "Error: %s", err) + } + pretty.Exit(0, "OK") + }, +} + +func init() { + feedbackCmd.AddCommand(issueCmd) + issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") + issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") +} diff --git a/cmd/variables.go b/cmd/variables.go index 42ef8703..45d45af8 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -59,7 +59,7 @@ func exportEnvironment(condaYaml []string, packfile, taskName, environment, work var data operations.Token if Has(packfile) { - config, err = robot.LoadRobotYaml(packfile) + config, err = robot.LoadRobotYaml(packfile, false) if err == nil { condaYaml = append(condaYaml, config.CondaConfigFile()) task = config.TaskByName(taskName) diff --git a/common/version.go b/common/version.go index f6e7f0a6..ddb77dd5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.2.0` + Version = `v9.3.0` ) diff --git a/conda/validate.go b/conda/validate.go index d44e1d48..0bb18628 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -15,13 +15,17 @@ var ( validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") ) +func ValidLocation(value string) bool { + return validPathCharacters.MatchString(value) +} + func validateLocations(checked map[string]string) bool { success := true for name, value := range checked { if len(value) == 0 { continue } - if !validPathCharacters.MatchString(value) { + if !ValidLocation(value) { success = false common.Log("%sWARNING! %s contain illegal characters. Cannot use tooling with path %q.%s", pretty.Yellow, name, value, pretty.Reset) } diff --git a/docs/changelog.md b/docs/changelog.md index 2b2da9a2..de60021a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v9.3.0 (date: 28.1.2021) + +- support for applications to submit issue reports thru rcc +- print "robot.yaml" to logs, to make it visible for support cases +- diagnostics can now print into a file, and that is used as part + of issue reporting +- added links to diagnostic checks, for user guidance + ## v9.2.0 (date: 25.1.2021) - experiment: carrier PoC diff --git a/operations/assistant.go b/operations/assistant.go index 32a56d0d..016289f9 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -147,14 +147,14 @@ func (it *ArtifactPublisher) Publish(fullpath, relativepath string, details os.F common.Log("ERR: did not get correct response postinfo in reply from cloud.") return //err } - err = multipartUpload(outcome.Response.PostInfo.Url, outcome.Response.PostInfo.Fields, basename, fullpath) + err = MultipartUpload(outcome.Response.PostInfo.Url, outcome.Response.PostInfo.Fields, basename, fullpath) if err != nil { it.ErrorCount += 1 common.Error("Assistant/Last", err) } } -func multipartUpload(url string, fields map[string]string, basename, fullpath string) error { +func MultipartUpload(url string, fields map[string]string, basename, fullpath string) error { buffer := new(bytes.Buffer) many := multipart.NewWriter(buffer) diff --git a/operations/authorize.go b/operations/authorize.go index bfce4f5b..f037c2a2 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -50,6 +50,14 @@ func (it Token) AsJson() (string, error) { return string(body), nil } +func (it Token) FromJson(content []byte) error { + err := json.Unmarshal(content, &it) + if err != nil { + return err + } + return nil +} + type UserInfo struct { User Token `json:"user"` Link Token `json:"request"` diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 88c2f3b5..6532ef52 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -3,19 +3,29 @@ package operations import ( "encoding/json" "fmt" + "io" "net" "os" "runtime" + "sort" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/xviper" ) const ( - canaryHost = `https://downloads.robocorp.com` - canaryUrl = `/canary.txt` + canaryHost = `https://downloads.robocorp.com` + canaryUrl = `/canary.txt` + supportLongPathUrl = `https://robocorp.com/docs/troubleshooting/windows-long-path` + supportNetworkUrl = `https://robocorp.com/docs/troubleshooting/firewall-and-proxies` + supportGeneralUrl = `https://robocorp.com/docs/troubleshooting` + statusOk = `ok` + statusWarning = `warning` + statusFail = `fail` + statusFatal = `fatal` ) var ( @@ -38,6 +48,7 @@ type DiagnosticCheck struct { Type string `json:"type"` Status string `json:"status"` Message string `json:"message"` + Link string `json:"url"` } func (it *DiagnosticStatus) AsJson() (string, error) { @@ -69,12 +80,15 @@ func RunDiagnostics() *DiagnosticStatus { result.Details["user-cache-dir"] = justText(os.UserCacheDir) result.Details["user-config-dir"] = justText(os.UserConfigDir) result.Details["user-home-dir"] = justText(os.UserHomeDir) - //result.Details["hostname"] = justText(os.Hostname) + result.Details["working-dir"] = justText(os.Getwd) result.Details["tempdir"] = os.TempDir() + result.Details["controller"] = common.ControllerIdentity() + result.Details["installationId"] = xviper.TrackingIdentity() result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) // checks + result.Checks = append(result.Checks, robocorpHomeCheck()) result.Checks = append(result.Checks, longPathSupportCheck()) for _, host := range checkedHosts { result.Checks = append(result.Checks, dnsLookupCheck(host)) @@ -98,14 +112,33 @@ func longPathSupportCheck() *DiagnosticCheck { if conda.HasLongPathSupport() { return &DiagnosticCheck{ Type: "OS", - Status: "ok", + Status: statusOk, Message: "Supports long enough paths.", + Link: supportLongPathUrl, } } return &DiagnosticCheck{ Type: "OS", - Status: "fail", + Status: statusFail, Message: "Does not support long path names!", + Link: supportLongPathUrl, + } +} + +func robocorpHomeCheck() *DiagnosticCheck { + if !conda.ValidLocation(conda.RobocorpHome()) { + return &DiagnosticCheck{ + Type: "RPA", + Status: statusFatal, + Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", conda.RobocorpHome()), + Link: supportGeneralUrl, + } + } + return &DiagnosticCheck{ + Type: "RPA", + Status: statusOk, + Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", conda.RobocorpHome()), + Link: supportGeneralUrl, } } @@ -114,14 +147,16 @@ func dnsLookupCheck(site string) *DiagnosticCheck { if err != nil { return &DiagnosticCheck{ Type: "network", - Status: "fail", + Status: statusFail, Message: fmt.Sprintf("DNS lookup %s failed: %v", site, err), + Link: supportNetworkUrl, } } return &DiagnosticCheck{ Type: "network", - Status: "ok", + Status: statusOk, Message: fmt.Sprintf("%s found: %v", site, found), + Link: supportNetworkUrl, } } @@ -130,8 +165,9 @@ func canaryDownloadCheck() *DiagnosticCheck { if err != nil { return &DiagnosticCheck{ Type: "network", - Status: "fail", + Status: statusFail, Message: fmt.Sprintf("%v: %v", canaryHost, err), + Link: supportNetworkUrl, } } request := client.NewRequest(canaryUrl) @@ -139,13 +175,67 @@ func canaryDownloadCheck() *DiagnosticCheck { if response.Status != 200 || string(response.Body) != "Used to testing connections" { return &DiagnosticCheck{ Type: "network", - Status: "fail", + Status: statusFail, Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), + Link: supportNetworkUrl, } } return &DiagnosticCheck{ Type: "network", - Status: "ok", + Status: statusOk, Message: fmt.Sprintf("Canary download successful: %s%s", canaryHost, canaryUrl), + Link: supportNetworkUrl, + } +} + +func jsonDiagnostics(sink io.Writer, details *DiagnosticStatus) { + form, err := details.AsJson() + if err != nil { + pretty.Exit(1, "Error: %s", err) + } + fmt.Fprintln(sink, form) +} + +func humaneDiagnostics(sink io.Writer, details *DiagnosticStatus) { + fmt.Fprintln(sink, "Diagnostics:") + keys := make([]string, 0, len(details.Details)) + for key, _ := range details.Details { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + value := details.Details[key] + fmt.Fprintf(sink, " - %-18s... %q\n", key, value) + } + fmt.Fprintln(sink, "") + fmt.Fprintln(sink, "Checks:") + for _, check := range details.Checks { + fmt.Fprintf(sink, " - %-8s %-8s %s\n", check.Type, check.Status, check.Message) + } +} + +func fileIt(filename string) (io.WriteCloser, error) { + if len(filename) == 0 { + return os.Stdout, nil + } + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return nil, err + } + return file, nil +} + +func PrintDiagnostics(filename string, json bool) error { + file, err := fileIt(filename) + if err != nil { + return err + } + defer file.Close() + result := RunDiagnostics() + if json { + jsonDiagnostics(file, result) + } else { + humaneDiagnostics(file, result) } + return nil } diff --git a/operations/fixing.go b/operations/fixing.go index 2aedfc88..3e614f22 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -75,7 +75,7 @@ func ensureFilesExecutable(dir string) { } func FixRobot(robotFile string) error { - config, err := robot.LoadRobotYaml(robotFile) + config, err := robot.LoadRobotYaml(robotFile, false) if err != nil { return err } diff --git a/operations/issues.go b/operations/issues.go new file mode 100644 index 00000000..81847bf8 --- /dev/null +++ b/operations/issues.go @@ -0,0 +1,135 @@ +package operations + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "runtime" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/xviper" +) + +const ( + issueHost = `https://telemetry.robocorp.com` + issueUrl = `/diagnostics-v1/issue` +) + +func loadToken(reportFile string) (Token, error) { + content, err := ioutil.ReadFile(reportFile) + if err != nil { + return nil, err + } + token := make(Token) + err = token.FromJson(content) + if err != nil { + return nil, err + } + return token, nil +} + +func createIssueZip(attachmentsFiles []string) (string, error) { + zipfile := filepath.Join(conda.RobocorpTemp(), "attachments.zip") + zipper, err := newZipper(zipfile) + if err != nil { + return "", err + } + defer zipper.Close() + for index, attachment := range attachmentsFiles { + niceName := fmt.Sprintf("%x_%s", index+1, filepath.Base(attachment)) + zipper.Add(attachment, niceName, nil) + } + return zipfile, nil +} + +func createDiagnosticsReport() (string, error) { + file := filepath.Join(conda.RobocorpTemp(), "diagnostics.txt") + err := PrintDiagnostics(file, false) + if err != nil { + return "", err + } + return file, nil +} + +func virtualName(filename string) (string, error) { + digest, err := pathlib.Sha256(filename) + if err != nil { + return "", err + } + return fmt.Sprintf("attachments_%s.zip", digest[:16]), nil +} + +func ReportIssue(reportFile string, attachmentsFiles []string) error { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) + token, err := loadToken(reportFile) + if err != nil { + return err + } + diagnostics, err := createDiagnosticsReport() + if err == nil { + attachmentsFiles = append(attachmentsFiles, diagnostics) + } + attachmentsFiles = append(attachmentsFiles, reportFile) + filename, err := createIssueZip(attachmentsFiles) + if err != nil { + return err + } + shortname, err := virtualName(filename) + if err != nil { + return err + } + installationId := xviper.TrackingIdentity() + token["installationId"] = installationId + token["fileName"] = shortname + token["controller"] = common.ControllerIdentity() + _, ok := token["platform"] + if !ok { + token["platform"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) + } + issueReport, err := token.AsJson() + if err != nil { + return err + } + common.Trace(issueReport) + client, err := cloud.NewClient(issueHost) + if err != nil { + return err + } + request := client.NewRequest(issueUrl) + request.Headers[contentType] = applicationJson + request.Body = bytes.NewBuffer([]byte(issueReport)) + response := client.Post(request) + json := make(Token) + err = json.FromJson(response.Body) + if err != nil { + return err + } + postInfo, ok := json["attachmentPostInfo"].(map[string]interface{}) + if !ok { + return fmt.Errorf("Could not get attachmentPostInfo!") + } + url, ok := postInfo["url"].(string) + if !ok { + return fmt.Errorf("Could not get URL from attachmentPostInfo!") + } + fields, ok := postInfo["fields"].(map[string]interface{}) + if !ok { + return fmt.Errorf("Could not get fields from attachmentPostInfo!") + } + return MultipartUpload(url, toStringMap(fields), shortname, filename) +} + +func toStringMap(entries map[string]interface{}) map[string]string { + result := make(map[string]string) + for key, value := range entries { + text, ok := value.(string) + if ok { + result[key] = text + } + } + return result +} diff --git a/operations/running.go b/operations/running.go index 21895afa..dfdfaab4 100644 --- a/operations/running.go +++ b/operations/running.go @@ -46,7 +46,7 @@ func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, enviro func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot.Robot, robot.Task, string) { FixRobot(packfile) - config, err := robot.LoadRobotYaml(packfile) + config, err := robot.LoadRobotYaml(packfile, true) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/operations/zipper.go b/operations/zipper.go index 860e8dee..9026d301 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -173,7 +173,11 @@ func (it *zipper) Note(err error) { } func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { - common.Debug("- %v size %v", relativepath, details.Size()) + if details != nil { + common.Debug("- %v size %v", relativepath, details.Size()) + } else { + common.Debug("- %v", relativepath) + } source, err := os.Open(fullpath) if err != nil { it.Note(err) @@ -281,7 +285,7 @@ func Unzip(directory, zipfile string, force, temporary bool) error { func Zip(directory, zipfile string, ignores []string) error { common.Debug("Wrapping %v into %v ...", directory, zipfile) - config, err := robot.LoadRobotYaml(robot.DetectConfigurationName(directory)) + config, err := robot.LoadRobotYaml(robot.DetectConfigurationName(directory), false) if err != nil { return err } diff --git a/robot/robot.go b/robot/robot.go index c4eddd59..a74901c7 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -300,7 +300,7 @@ func PlainEnvironment(inject []string, full bool) []string { return environment } -func LoadRobotYaml(filename string) (Robot, error) { +func LoadRobotYaml(filename string, visible bool) (Robot, error) { fullpath, err := filepath.Abs(filename) if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) @@ -309,6 +309,9 @@ func LoadRobotYaml(filename string) (Robot, error) { if err != nil { return nil, fmt.Errorf("%q: %w", fullpath, err) } + if visible { + common.Log("%q as robot.yaml is:\n%s", fullpath, string(content)) + } robot, err := robotFrom(content) if err != nil { return nil, fmt.Errorf("%q: %w", fullpath, err) diff --git a/robot/robot_test.go b/robot/robot_test.go index 32bb1ce5..3f013561 100644 --- a/robot/robot_test.go +++ b/robot/robot_test.go @@ -11,7 +11,7 @@ import ( func TestCannotReadMissingRobotYaml(t *testing.T) { must, wont := hamlet.Specifications(t) - sut, err := robot.LoadRobotYaml("testdata/badmissing.yaml") + sut, err := robot.LoadRobotYaml("testdata/badmissing.yaml", false) wont.Nil(err) must.Nil(sut) } @@ -19,7 +19,7 @@ func TestCannotReadMissingRobotYaml(t *testing.T) { func TestCanReadRealRobotYaml(t *testing.T) { must, wont := hamlet.Specifications(t) - sut, err := robot.LoadRobotYaml("testdata/robot.yaml") + sut, err := robot.LoadRobotYaml("testdata/robot.yaml", false) must.Nil(err) wont.Nil(sut) must.Equal(1, len(sut.IgnoreFiles())) @@ -44,7 +44,7 @@ func TestCanReadRealRobotYaml(t *testing.T) { func TestCanGetShellFormCommand(t *testing.T) { must, wont := hamlet.Specifications(t) - sut, err := robot.LoadRobotYaml("testdata/robot.yaml") + sut, err := robot.LoadRobotYaml("testdata/robot.yaml", false) must.Nil(err) wont.Nil(sut) task := sut.TaskByName("Shell Form Name") @@ -59,7 +59,7 @@ func TestCanGetShellFormCommand(t *testing.T) { func TestCanGetTaskFormCommand(t *testing.T) { must, wont := hamlet.Specifications(t) - sut, err := robot.LoadRobotYaml("testdata/robot.yaml") + sut, err := robot.LoadRobotYaml("testdata/robot.yaml", false) must.Nil(err) wont.Nil(sut) task := sut.TaskByName("Task Form Name") diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6f65483e..6314ef09 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -27,6 +27,7 @@ Using and running template example with shell file Goal Send telemetry data to cloud. Step build/rcc feedback metric --controller citests -t test -n rcc.test -v robot.fullrun + Use STDERR Must Have OK Goal Telemetry tracking can be disabled. @@ -35,6 +36,7 @@ Using and running template example with shell file Goal Show listing of rcc commands. Step build/rcc --controller citests + Use STDERR Must Have rcc is environment manager Wont Have missing @@ -52,10 +54,12 @@ Using and running template example with shell file Must Have extended Must Have python Must Have standard + Use STDERR Must Have OK. Goal Initialize new standard robot into tmp/fluffy folder using force. Step build/rcc robot init --controller citests -t extended -d tmp/fluffy -f + Use STDERR Must Have OK. Goal There should now be fluffy in robot listing @@ -66,15 +70,17 @@ Using and running template example with shell file Goal Fail to initialize new standard robot into tmp/fluffy without force. Step build/rcc robot init --controller citests -t extended -d tmp/fluffy 2 + Use STDERR Must Have Error: Directory Must Have fluffy is not empty Goal Run task in place. Step build/rcc task run --controller citests -r tmp/fluffy/robot.yaml + Must Have 1 critical task, 1 passed, 0 failed + Use STDERR Must Have Progress: 0/5 Must Have Progress: 5/5 Must Have rpaframework - Must Have 1 critical task, 1 passed, 0 failed Must Have OK. Must Exist %{ROBOCORP_HOME}/base/ Must Exist %{ROBOCORP_HOME}/live/ @@ -83,18 +89,20 @@ Using and running template example with shell file Goal Run task in clean temporary directory. Step build/rcc task testrun --controller citests -r tmp/fluffy/robot.yaml + Must Have 1 critical task, 1 passed, 0 failed + Use STDERR + Must Have rpaframework Must Have Progress: 0/5 Wont Have Progress: 1/5 Wont Have Progress: 2/5 Wont Have Progress: 3/5 Wont Have Progress: 4/5 Must Have Progress: 5/5 - Must Have rpaframework - Must Have 1 critical task, 1 passed, 0 failed Must Have OK. Goal Merge two different conda.yaml files with conflict fails Step build/rcc env new --controller citests conda/testdata/conda.yaml conda/testdata/other.yaml 1 + Use STDERR Must Have robotframework=3.1 vs. robotframework=3.2 Goal Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/lease.robot b/robot_tests/lease.robot index e6b3160a..23206367 100644 --- a/robot_tests/lease.robot +++ b/robot_tests/lease.robot @@ -11,6 +11,7 @@ Can operate leased environments Goal Check listing for taker information Step build/rcc env list + Use STDERR Must Have "taker (1)" Goal Others can get same environment @@ -25,6 +26,7 @@ Can operate leased environments Goal Check listing for taker information (still same) Step build/rcc env list + Use STDERR Must Have "taker (1)" Wont Have "second (2)" @@ -34,32 +36,38 @@ Can operate leased environments Goal Now others cannot get same environment anymore Step build/rcc env variables robot_tests/leasebot/conda.yaml 1 - Must Have Environment leased to "taker (1)" is dirty Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef Wont Have CONDA_DEFAULT_ENV=rcc + Use STDERR + Must Have Environment leased to "taker (1)" is dirty Goal Cannot share environment, since it is dirty Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml 1 - Must Have Cannot get environment "8f1d3dc95228edef" because it is dirty and leased by "taker (1)" Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef Wont Have CONDA_DEFAULT_ENV=rcc + Use STDERR + Must Have Cannot get environment "8f1d3dc95228edef" because it is dirty and leased by "taker (1)" Goal Cannot unlease someone elses environment Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef 1 + Use STDERR Must Have Error: Goal Cannot delete someone elses leased environment Step build/rcc env delete 8f1d3dc95228edef 1 + Use STDERR Must Have WARNING: "8f1d3dc95228edef" is leased by "taker (1)" and wont be deleted! Goal Check listing for taker information (still same) Step build/rcc env list + Use STDERR Must Have 8f1d3dc95228edef Must Have "taker (1)" Wont Have "second (2)" Goal Lease can be unleased Step build/rcc env unlease --lease "taker (1)" --hash 8f1d3dc95228edef + Use STDERR Must Have OK. Goal Others can now lease that environment @@ -69,9 +77,11 @@ Can operate leased environments Goal Check listing for taker information (still same) Step build/rcc env list + Use STDERR Must Have "second (2)" Wont Have "taker (1)" Goal Lease can be unleased Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef + Use STDERR Must Have OK. diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index fe7c990e..b280dc45 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -29,12 +29,21 @@ Goal Step [Arguments] ${command} ${expected}=0 - ${code} ${output}= Run and return rc and output ${command} - Set Suite Variable ${robot_output} ${output} - Log
${output}
html=yes + ${code} ${output} ${error}= Run and return code output error ${command} + Set Suite Variable ${robot_stdout} ${output} + Set Suite Variable ${robot_stderr} ${error} + Use Stdout + Log STDOUT
${output}
html=yes + Log STDERR
${error}
html=yes Should be equal as strings ${expected} ${code} Wont Have Failure: +Use Stdout + Set Suite Variable ${robot_output} ${robot_stdout} + +Use Stderr + Set Suite Variable ${robot_output} ${robot_stderr} + Must Be [Arguments] ${content} Should Be Equal As Strings ${robot_output} ${content} diff --git a/robot_tests/supporting.py b/robot_tests/supporting.py index 9cb97652..f2ea9405 100644 --- a/robot_tests/supporting.py +++ b/robot_tests/supporting.py @@ -1,4 +1,10 @@ import json +import subprocess + +def run_and_return_code_output_error(command): + task = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + out, err = task.communicate() + return task.returncode, out.decode(), err.decode() def parse_json(content): parsed = json.loads(content) From 9224b303ce927566e26f6440ee7c95905e4b5372 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 29 Jan 2021 10:16:58 +0200 Subject: [PATCH 056/516] RCC-136: leased temp is not recycled (v9.3.1) - fix: when environment is leased, temporary folder is will not be recycled - cleanup command now cleans also temporary folders based on day limit --- cmd/rcc/main.go | 2 +- common/variables.go | 1 + common/version.go | 2 +- conda/cleanup.go | 35 +++++++++++++++++++++++++++++++++++ conda/lease.go | 8 +++++++- docs/changelog.md | 5 +++++ 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index a5de40bd..986d5bd1 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -45,7 +45,7 @@ func startTempRecycling() { } func markTempForRecycling() { - if markedAlready { + if common.LeaseEffective || markedAlready { return } markedAlready = true diff --git a/common/variables.go b/common/variables.go index 68ee6b2e..580027de 100644 --- a/common/variables.go +++ b/common/variables.go @@ -12,6 +12,7 @@ var ( NoCache bool Liveonly bool Stageonly bool + LeaseEffective bool StageFolder string ControllerType string LeaseContract string diff --git a/common/version.go b/common/version.go index ddb77dd5..9a117203 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.0` + Version = `v9.3.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index bdd629b7..ffbc2515 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -3,6 +3,7 @@ package conda import ( "fmt" "os" + "path/filepath" "time" "github.com/robocorp/rcc/common" @@ -103,6 +104,39 @@ func anyLeasedEnvironment() bool { return false } +func cleanupTemp(deadline time.Time, dryrun bool) error { + basedir := RobocorpTempRoot() + handle, err := os.Open(basedir) + if err != nil { + return err + } + entries, err := handle.Readdir(-1) + handle.Close() + if err != nil { + return err + } + for _, entry := range entries { + if entry.ModTime().After(deadline) { + continue + } + fullpath := filepath.Join(basedir, entry.Name()) + if dryrun { + common.Log("Would remove temp %v.", fullpath) + continue + } + if entry.IsDir() { + err = os.RemoveAll(fullpath) + if err != nil { + common.Log("Warning[%q]: %v", fullpath, err) + } + } else { + os.Remove(fullpath) + } + common.Debug("Removed %v.", fullpath) + } + return nil +} + func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) error { lockfile := RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) @@ -118,6 +152,7 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) err } deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) + cleanupTemp(deadline, dryrun) for _, template := range TemplateList() { whenLive, err := LastUsed(LiveFrom(template)) if err != nil { diff --git a/conda/lease.go b/conda/lease.go index 15484e76..2f1fb2ab 100644 --- a/conda/lease.go +++ b/conda/lease.go @@ -57,11 +57,17 @@ func CouldExtendLease(hash string) bool { return false } pathlib.TouchWhen(LeaseFileFrom(hash), time.Now()) + common.LeaseEffective = true return true } func TakeLease(hash, reason string) error { - return writeLeaseFile(hash, reason) + err := writeLeaseFile(hash, reason) + if err != nil { + return err + } + common.LeaseEffective = true + return nil } func DropLease(hash, reason string) error { diff --git a/docs/changelog.md b/docs/changelog.md index de60021a..1538dcec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.3.1 (date: 29.1.2021) + +- fix: when environment is leased, temporary folder is will not be recycled +- cleanup command now cleans also temporary folders based on day limit + ## v9.3.0 (date: 28.1.2021) - support for applications to submit issue reports thru rcc From d8430ad65ee9cc1d13a77de40d32cb825242556a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 29 Jan 2021 13:27:55 +0200 Subject: [PATCH 057/516] RCC-137: more environment variables (v9.3.2) - added environment variables for installation identity, opt-out status as `RCC_INSTALLATION_ID` and `RCC_TRACKING_ALLOWED` --- common/version.go | 2 +- conda/robocorp.go | 3 +++ docs/changelog.md | 5 +++++ robot/robot.go | 3 +++ robot_tests/fullrun.robot | 6 ++++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 9a117203..85d1c8a3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.1` + Version = `v9.3.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index e8b677f4..ec159288 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -14,6 +14,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/shell" + "github.com/robocorp/rcc/xviper" ) const ( @@ -170,6 +171,8 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, + "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), + "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), "TEMP="+RobocorpTemp(), "TMP="+RobocorpTemp(), searchPath.AsEnvironmental("PATH"), diff --git a/docs/changelog.md b/docs/changelog.md index 1538dcec..740e2bb4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.3.2 (date: 29.1.2021) + +- added environment variables for installation identity, opt-out status as + `RCC_INSTALLATION_ID` and `RCC_TRACKING_ALLOWED` + ## v9.3.1 (date: 29.1.2021) - fix: when environment is leased, temporary folder is will not be recycled diff --git a/robot/robot.go b/robot/robot.go index a74901c7..bbeb7310 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/xviper" "github.com/google/shlex" "gopkg.in/yaml.v2" @@ -218,6 +219,8 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo "PYTHONNOUSERSITE=1", "ROBOCORP_HOME="+conda.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, + "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), + "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), "TEMP="+conda.RobocorpTemp(), "TMP="+conda.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6314ef09..80cd47c4 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -124,6 +124,9 @@ Using and running template example with shell file Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= Must Have f0a9e281269b31ea @@ -147,6 +150,9 @@ Using and running template example with shell file Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= Must Have ROBOT_ROOT= Must Have ROBOT_ARTIFACTS= Wont Have RC_API_SECRET_HOST= From 6bd09b78412cb95096e34d38ae66a0fa2a2e2b4b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 1 Feb 2021 08:19:31 +0200 Subject: [PATCH 058/516] OTHER: README.md update - added link to latest version information text file in direct download table --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39d0a5ce..c3a941cc 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ Upgrading: `brew upgrade rcc` ### Direct downloads for signed executables provided by Robocorp -| OS | Download URL | -| ------- | -------------------------------------------------------------------- | -| Windows | https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe | -| macOS | https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc | -| Linux | https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc | +| OS | Download URL [latest version info](https://downloads.robocorp.com/rcc/releases/latest/version.txt) | +| -------- | --------------------------------------------------------------------------------------------------- | +| Windows | https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe | +| macOS | https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc | +| Linux | https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc | *[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* From 7081542aa4dab7be6633409bb045a023bd30de8f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 1 Feb 2021 12:02:28 +0200 Subject: [PATCH 059/516] RCC-138: issue reporting dryrun (v9.3.3) - adding `--dryrun` option to issue reporting --- cmd/issue.go | 3 ++- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/issues.go | 14 +++++++++++++- robot_tests/fullrun.robot | 10 ++++++++++ robot_tests/report.json | 5 +++++ 6 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 robot_tests/report.json diff --git a/cmd/issue.go b/cmd/issue.go index 51b3a4bb..7db9e4b0 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -21,7 +21,7 @@ var issueCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Feedback issue lasted").Report() } - err := operations.ReportIssue(issueMetafile, issueAttachments) + err := operations.ReportIssue(issueMetafile, issueAttachments, dryFlag) if err != nil { pretty.Exit(1, "Error: %s", err) } @@ -33,4 +33,5 @@ func init() { feedbackCmd.AddCommand(issueCmd) issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") + issueCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't send issue report, just show what would report be.") } diff --git a/common/version.go b/common/version.go index 85d1c8a3..bead21be 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.2` + Version = `v9.3.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 740e2bb4..fdd43756 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.3.3 (date: 1.2.2021) + +- adding `--dryrun` option to issue reporting + ## v9.3.2 (date: 29.1.2021) - added environment variables for installation identity, opt-out status as diff --git a/operations/issues.go b/operations/issues.go index 81847bf8..f7e1b9be 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io/ioutil" + "os" "path/filepath" "runtime" @@ -63,7 +64,7 @@ func virtualName(filename string) (string, error) { return fmt.Sprintf("attachments_%s.zip", digest[:16]), nil } -func ReportIssue(reportFile string, attachmentsFiles []string) error { +func ReportIssue(reportFile string, attachmentsFiles []string, dryrun bool) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) token, err := loadToken(reportFile) if err != nil { @@ -94,6 +95,17 @@ func ReportIssue(reportFile string, attachmentsFiles []string) error { if err != nil { return err } + if dryrun { + metaForm := make(Token) + metaForm["report"] = token + metaForm["zipfile"] = filename + report, err := metaForm.AsJson() + if err != nil { + return err + } + fmt.Fprintln(os.Stdout, report) + return nil + } common.Trace(issueReport) client, err := cloud.NewClient(issueHost) if err != nil { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 80cd47c4..913dfb8a 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -168,3 +168,13 @@ Using and running template example with shell file Goal See diagnostics as valid JSON form Step build/rcc configure diagnostics --json Must Be Json Response + + Goal Simulate issue report sending with dryrun + Step build/rcc feedback issue --dryrun --report robot_tests/report.json --attachments robot_tests/conda.yaml + Must Have "report": + Must Have "zipfile": + Must Have "installationId": + Must Have "platform": + Must Be Json Response + Use STDERR + Must Have OK diff --git a/robot_tests/report.json b/robot_tests/report.json new file mode 100644 index 00000000..67a84e02 --- /dev/null +++ b/robot_tests/report.json @@ -0,0 +1,5 @@ +{ + "errorCode": "test", + "errorName": "robot test", + "dialogMessage": "delete this, this it just a test issue (with attachment)" +} From b28562c78e59337fe27229cc4f036c651d25ddb9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 2 Feb 2021 09:21:38 +0200 Subject: [PATCH 060/516] RCC-140: rename environment before removal (v9.3.4) - fix: removing environments now uses rename first and then delete, to get around windows locked files issue - warning: on windows, if environment is somehow locked by some process, this will fail earlier in the process (which is good thing), so be aware - minor change on cache statistics representation and calculation --- cmd/clone.go | 5 +- common/version.go | 2 +- conda/workflows.go | 118 +++++++++++++++++++++++++++++++++------------ docs/changelog.md | 8 +++ 4 files changed, 101 insertions(+), 32 deletions(-) diff --git a/cmd/clone.go b/cmd/clone.go index 7f890942..2e759b2d 100644 --- a/cmd/clone.go +++ b/cmd/clone.go @@ -17,7 +17,10 @@ var cloneCmd = &cobra.Command{ source := cmd.LocalFlags().Lookup("source").Value.String() target := cmd.LocalFlags().Lookup("target").Value.String() defer common.Stopwatch("rcc internal clone lasted").Report() - success := conda.CloneFromTo(source, target, pathlib.CopyFile) + success, err := conda.CloneFromTo(source, target, pathlib.CopyFile) + if err != nil { + pretty.Exit(2, "Error: Cloning failed, reason: %v!", err) + } if !success { pretty.Exit(1, "Error: Cloning failed.") } diff --git a/common/version.go b/common/version.go index bead21be..3bc296e9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.3` + Version = `v9.3.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 86af631b..60e8b752 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "os" "path/filepath" "strings" @@ -62,17 +63,20 @@ func IsPristine(folder string) bool { return Hexdigest(digest) == meta } -func reuseExistingLive(key string) bool { +func reuseExistingLive(key string) (bool, error) { if common.Stageonly { - return false + return false, nil } candidate := LiveFrom(key) if IsPristine(candidate) { touchMetafile(candidate) - return true + return true, nil } - removeClone(candidate) - return false + err := removeClone(candidate) + if err != nil { + return false, err + } + return false, nil } func LiveExecution(liveFolder string, command ...string) error { @@ -117,20 +121,26 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { return false } -func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) bool { +func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, error) { targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") - removeClone(targetFolder) + err := removeClone(targetFolder) + if err != nil { + return false, err + } common.Debug("=== new live --- first try phase ===") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { common.Debug("=== new live --- second try phase ===") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") - removeClone(targetFolder) + err = removeClone(targetFolder) + if err != nil { + return false, err + } success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, postInstall) } - return success + return success, nil } func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { @@ -277,7 +287,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { freshInstall := templates == 0 defer func() { - common.Log("#### Progress: 5/5 [Done.] [Stats: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) + templates = len(TemplateList()) + common.Log("#### Progress: 5/5 [Done.] [Cache statistics: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) }() common.Log("#### Progress: 0/5 [try use existing live same environment?] %v", xviper.TrackingIdentity()) @@ -312,7 +323,11 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } liveFolder := LiveFrom(key) - if reuseExistingLive(key) { + reusable, err := reuseExistingLive(key) + if err != nil { + return "", err + } + if reusable { hits += 1 xviper.Set("stats.env.hit", hits) return liveFolder, nil @@ -321,21 +336,32 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { common.Log("#### Progress: 1/5 [skipped -- stage only]") } else { common.Log("#### Progress: 1/5 [try clone existing same template to live, key: %v]", key) - if CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) { + success, err := CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) + if err != nil { + return "", err + } + if success { dirty += 1 xviper.Set("stats.env.dirty", dirty) return liveFolder, nil } } common.Log("#### Progress: 2/5 [try create new environment from scratch]") - if newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) { + success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) + if err != nil { + return "", err + } + if success { misses += 1 xviper.Set("stats.env.miss", misses) if common.Liveonly { common.Log("#### Progress: 4/5 [skipped -- live only]") } else { common.Log("#### Progress: 4/5 [backup new environment as template]") - CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) + _, err = CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) + if err != nil { + return "", err + } } return liveFolder, nil } @@ -349,41 +375,73 @@ func RemoveEnvironment(label string) error { if IsLeasedEnvironment(label) { return fmt.Errorf("WARNING: %q is leased by %q and wont be deleted!", label, WhoLeased(label)) } - removeClone(LiveFrom(label)) - removeClone(TemplateFrom(label)) - return nil + err := removeClone(LiveFrom(label)) + if err != nil { + return err + } + return removeClone(TemplateFrom(label)) } -func removeClone(location string) { - os.Remove(metafile(location)) - os.RemoveAll(location) +func removeClone(location string) error { + if !pathlib.IsDir(location) { + return nil + } + randomLocation := fmt.Sprintf("%s.%08X", location, rand.Uint32()) + common.Debug("Rename/remove %q using %q as random name.", location, randomLocation) + err := os.Rename(location, randomLocation) + if err != nil { + common.Log("Rename %q -> %q failed as: %v!", location, randomLocation, err) + return err + } + err = os.RemoveAll(randomLocation) + if err != nil { + common.Log("Removal of %q failed as: %v!", randomLocation, err) + return err + } + meta := metafile(location) + if pathlib.IsFile(meta) { + return os.Remove(meta) + } + return nil } -func CloneFromTo(source, target string, copier pathlib.Copier) bool { - removeClone(target) +func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { + err := removeClone(target) + if err != nil { + return false, err + } os.MkdirAll(target, 0755) if !IsPristine(source) { - removeClone(source) - return false + err = removeClone(source) + if err != nil { + return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) + } + return false, nil } expected, err := metaLoad(source) if err != nil { - return false + return false, nil } success := cloneFolder(source, target, 8, copier) if !success { - removeClone(target) - return false + err = removeClone(target) + if err != nil { + return false, fmt.Errorf("Cloning %q to %q failed! And cleanup failed: %v", source, target, err) + } + return false, nil } digest, err := DigestFor(target) if err != nil || Hexdigest(digest) != expected { - removeClone(target) - return false + err = removeClone(target) + if err != nil { + return false, fmt.Errorf("Target %q does not match source %q! And cleanup failed: %v!", target, source, err) + } + return false, nil } metaSave(target, expected) touchMetafile(source) - return true + return true, nil } func cloneFolder(source, target string, workers int, copier pathlib.Copier) bool { diff --git a/docs/changelog.md b/docs/changelog.md index fdd43756..edb99181 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v9.3.4 (date: 1.2.2021) + +- fix: removing environments now uses rename first and then delete, + to get around windows locked files issue +- warning: on windows, if environment is somehow locked by some process, + this will fail earlier in the process (which is good thing), so be aware +- minor change on cache statistics representation and calculation + ## v9.3.3 (date: 1.2.2021) - adding `--dryrun` option to issue reporting From 1f58b53feb29ca5de016f179fa938d3006be3097 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 2 Feb 2021 14:16:16 +0200 Subject: [PATCH 061/516] RCC-144: micromamba 0.7.12 dependency (v9.3.5) - micromamba dependency upgrade to 0.7.12 - REGRESSION: `rcc task shell` got broken when micromamba was introduced, and this version fixes that --- common/version.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 6 ++++++ shell/task.go | 14 ++++++++------ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/common/version.go b/common/version.go index 3bc296e9..a7167a8a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.4` + Version = `v9.3.5` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index ec159288..6a8b5484 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -225,7 +225,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 7010 + goodEnough := version >= 7012 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/docs/changelog.md b/docs/changelog.md index edb99181..c6a1242e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.3.5 (date: 2.2.2021) + +- micromamba upgrade to 0.7.12 +- REGRESSION: `rcc task shell` got broken when micromamba was introduced, + and this version fixes that + ## v9.3.4 (date: 1.2.2021) - fix: removing environments now uses rename first and then delete, diff --git a/shell/task.go b/shell/task.go index ce13aa61..0d32ae0a 100644 --- a/shell/task.go +++ b/shell/task.go @@ -81,24 +81,26 @@ func (it *Task) Tee(folder string, interactive bool) (int, error) { defer errfile.Close() stdout := io.MultiWriter(it.stdout(), outfile) stderr := io.MultiWriter(os.Stderr, errfile) + var stdin io.Reader = os.Stdin if !interactive { - os.Stdin.Close() + stdin = bytes.NewReader([]byte{}) } - return it.execute(os.Stdin, stdout, stderr) + return it.execute(stdin, stdout, stderr) } func (it *Task) Observed(sink io.Writer, interactive bool) (int, error) { stdout := io.MultiWriter(it.stdout(), sink) stderr := io.MultiWriter(os.Stderr, sink) + var stdin io.Reader = os.Stdin if !interactive { - os.Stdin.Close() + stdin = bytes.NewReader([]byte{}) } - return it.execute(os.Stdin, stdout, stderr) + return it.execute(stdin, stdout, stderr) } func (it *Task) CaptureOutput() (string, int, error) { - os.Stdin.Close() + stdin := bytes.NewReader([]byte{}) stdout := bytes.NewBuffer(nil) - code, err := it.execute(os.Stdin, stdout, os.Stderr) + code, err := it.execute(stdin, stdout, os.Stderr) return stdout.String(), code, err } From 0f29d1483e4007ec0f159b5f0e57c22c98586de1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 3 Feb 2021 15:19:25 +0200 Subject: [PATCH 062/516] RCC-145: remove defaults channel (v9.3.6) - removing "defaults" channel from robot templates --- common/version.go | 2 +- conda/condayaml.go | 2 +- docs/changelog.md | 4 ++++ templates/extended/conda.yaml | 3 +-- templates/python/conda.yaml | 3 +-- templates/standard/conda.yaml | 3 +-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/common/version.go b/common/version.go index a7167a8a..70bbe2d9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.5` + Version = `v9.3.6` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 702be87e..395a3792 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -123,7 +123,7 @@ func SummonEnvironment(filename string) *Environment { } } return &Environment{ - Channels: []string{"defaults", "conda-forge"}, + Channels: []string{"conda-forge"}, Conda: []*Dependency{}, Pip: []*Dependency{}, } diff --git a/docs/changelog.md b/docs/changelog.md index c6a1242e..d83594e4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.3.6 (date: 3.2.2021) + +- removing "defaults" channel from robot templates + ## v9.3.5 (date: 2.2.2021) - micromamba upgrade to 0.7.12 diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index e0078d4f..56b4a75b 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -1,7 +1,6 @@ channels: # Define conda channels here. - conda-forge - - defaults dependencies: # Define conda packages here. @@ -13,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index e0078d4f..56b4a75b 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -1,7 +1,6 @@ channels: # Define conda channels here. - conda-forge - - defaults dependencies: # Define conda packages here. @@ -13,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index e0078d4f..56b4a75b 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -1,7 +1,6 @@ channels: # Define conda channels here. - conda-forge - - defaults dependencies: # Define conda packages here. @@ -13,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html \ No newline at end of file + - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html From 704cda8a6888988128eb5e7375fcc630b5183308 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 4 Feb 2021 10:00:32 +0200 Subject: [PATCH 063/516] RCC-146: micromamba version detection fix (v9.3.7) - micromamba version printout changed, so rcc now parses new format - micromamba is 0.x, so it does not follow semantic versioning yet, so rcc will now "lockstep" versions, with micromamba locked to 0.7.12 now --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 4 ++++ docs/changelog.md | 6 ++++++ 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index 70bbe2d9..f145041e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.6` + Version = `v9.3.7` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 79442cbb..49e73eb9 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/stable/macos64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.7.12/macos64/micromamba" } func IsPosix() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 3027a89f..ee67bafb 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -18,7 +18,7 @@ var ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/stable/linux64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.7.12/linux64/micromamba" } func ExpandPath(entry string) string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 619cc0a2..e787416f 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -24,7 +24,7 @@ const ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/stable/windows64/micromamba.exe" + return "https://downloads.robocorp.com/micromamba/v0.7.12/windows64/micromamba.exe" } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 6a8b5484..d761108b 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -194,6 +194,10 @@ func MambaCache() string { func asVersion(text string) (uint64, string) { text = strings.TrimSpace(text) + multiline := strings.SplitN(text, "\n", 2) + if len(multiline) > 0 { + text = strings.TrimSpace(multiline[0]) + } parts := strings.SplitN(text, ".", 4) steps := len(parts) multipliers := []uint64{1000000, 1000, 1} diff --git a/docs/changelog.md b/docs/changelog.md index d83594e4..de9700d8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.3.7 (date: 4.2.2021) + +- micromamba version printout changed, so rcc now parses new format +- micromamba is 0.x, so it does not follow semantic versioning yet, so + rcc will now "lockstep" versions, with micromamba locked to 0.7.12 now + ## v9.3.6 (date: 3.2.2021) - removing "defaults" channel from robot templates From 33333ff55af50fdf86589c2da09ddcd1af4186a8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 4 Feb 2021 13:55:40 +0200 Subject: [PATCH 064/516] RCC-142: making subprocess PIDs visible (v9.3.8) - making started and finished subprocess PIDs visible in --debug level. --- common/version.go | 2 +- docs/changelog.md | 4 ++++ shell/task.go | 10 +++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index f145041e..24ca8d78 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.7` + Version = `v9.3.8` ) diff --git a/docs/changelog.md b/docs/changelog.md index de9700d8..65e901b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.3.8 (date: 4.2.2021) + +- making started and finished subprocess PIDs visible in --debug level. + ## v9.3.7 (date: 4.2.2021) - micromamba version printout changed, so rcc now parses new format diff --git a/shell/task.go b/shell/task.go index 0d32ae0a..ac5007b2 100644 --- a/shell/task.go +++ b/shell/task.go @@ -49,7 +49,15 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) command.Stdin = stdin command.Stdout = stdout command.Stderr = stderr - err := command.Run() + err := command.Start() + if err != nil { + return -500, err + } + common.Debug("PID #%d is %q.", command.Process.Pid, command) + defer func() { + common.Debug("PID #%d finished: %v.", command.Process.Pid, command.ProcessState) + }() + err = command.Wait() exit, ok := err.(*exec.ExitError) if ok { return exit.ExitCode(), err From 78edf65925b15302944282f5b6c948cdacc6b342 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 8 Feb 2021 13:22:40 +0200 Subject: [PATCH 065/516] OTHER: micromamba bug fixes (v9.3.9) - micromamba cleanup bug fix (got error if micromamba is missing) - micromamba download bug fix (killed on MacOS) --- common/version.go | 2 +- conda/cleanup.go | 66 ++++++++++++++++++--------------------------- conda/download.go | 4 +++ docs/changelog.md | 5 ++++ pretty/functions.go | 5 ++++ 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/common/version.go b/common/version.go index 24ca8d78..5c61c22b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.8` + Version = `v9.3.9` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index ffbc2515..09b7a56d 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -8,8 +8,27 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) +func safeRemove(hint, pathling string) { + var err error + if !pathlib.Exists(pathling) { + common.Debug("[%s] Missing %v, not need to remove.", hint, pathling) + return + } + if pathlib.IsDir(pathling) { + err = os.RemoveAll(pathling) + } else { + err = os.Remove(pathling) + } + if err != nil { + pretty.Warning("[%s] %s -> %v", hint, pathling, err) + } else { + common.Debug("[%s] Removed %v.", hint, pathling) + } +} + func doCleanup(fullpath string, dryrun bool) error { if !pathlib.Exists(fullpath) { return nil @@ -34,16 +53,7 @@ func orphanCleanup(dryrun bool) error { return nil } for _, orphan := range orphans { - var err error - if pathlib.IsDir(orphan) { - err = os.RemoveAll(orphan) - } else { - err = os.Remove(orphan) - } - if err != nil { - return err - } - common.Debug("Removed orphan %v.", orphan) + safeRemove("orphan", orphan) } return nil } @@ -62,36 +72,12 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", RobocorpTempRoot()) return nil } - err := os.RemoveAll(TemplateLocation()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", TemplateLocation()) - err = os.RemoveAll(LiveLocation()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", LiveLocation()) - err = os.RemoveAll(PipCache()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", PipCache()) - err = os.RemoveAll(MambaPackages()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", MambaPackages()) - err = os.Remove(BinMicromamba()) - if err != nil { - return err - } - common.Debug("Removed executable %v.", BinMicromamba()) - err = os.RemoveAll(RobocorpTempRoot()) - if err != nil { - return err - } - common.Debug("Removed directory %v.", RobocorpTempRoot()) + safeRemove("cache", TemplateLocation()) + safeRemove("cache", LiveLocation()) + safeRemove("cache", PipCache()) + safeRemove("cache", MambaPackages()) + safeRemove("temp", RobocorpTempRoot()) + safeRemove("executable", BinMicromamba()) return nil } diff --git a/conda/download.go b/conda/download.go index c884036b..4c3be77e 100644 --- a/conda/download.go +++ b/conda/download.go @@ -20,6 +20,10 @@ func DownloadMicromamba() error { } defer response.Body.Close() + if pathlib.Exists(BinMicromamba()) { + os.Remove(BinMicromamba()) + } + pathlib.EnsureDirectory(filepath.Dir(BinMicromamba())) out, err := os.Create(filename) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index 65e901b6..4df1a664 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.3.9 (date: 8.2.2021) + +- micromamba cleanup bug fix (got error if micromamba is missing) +- micromamba download bug fix (killed on MacOS) + ## v9.3.8 (date: 4.2.2021) - making started and finished subprocess PIDs visible in --debug level. diff --git a/pretty/functions.go b/pretty/functions.go index 2441f870..769c372a 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -11,6 +11,11 @@ func Ok() error { return nil } +func Warning(format string, rest ...interface{}) { + niceform := fmt.Sprintf("%sWarning: %s%s", Yellow, format, Reset) + common.Log(niceform, rest...) +} + func Exit(code int, format string, rest ...interface{}) { var niceform string if code == 0 { From 8d1d69e091eef129c01c33cf49974f60a9625af3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 11 Feb 2021 10:14:32 +0200 Subject: [PATCH 066/516] BUGFIX: comtypes bug fix (v9.3.10) - Windows automation made environments dirty by generating comtypes/gen folder. Fix is to ignore that folder. - Added some more diagnostics information. --- common/version.go | 2 +- conda/robocorp.go | 8 +++++++- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 5c61c22b..0034ccf7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.9` + Version = `v9.3.10` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index d761108b..bbb276a0 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -38,6 +38,12 @@ func sorted(files []os.FileInfo) { }) } +func ignoreDynamicDirectories(folder, entryName string) bool { + base := strings.ToLower(filepath.Base(folder)) + name := strings.ToLower(entryName) + return name == "__pycache__" || (name == "gen" && base == "comtypes") +} + func DigestFor(folder string) ([]byte, error) { handle, err := os.Open(folder) if err != nil { @@ -52,7 +58,7 @@ func DigestFor(folder string) ([]byte, error) { sorted(entries) for _, entry := range entries { if entry.IsDir() { - if entry.Name() == "__pycache__" { + if ignoreDynamicDirectories(folder, entry.Name()) { continue } digest, err := DigestFor(filepath.Join(folder, entry.Name())) diff --git a/docs/changelog.md b/docs/changelog.md index 4df1a664..4d31e2bf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.3.10 (date: 11.2.2021) + +- Windows automation made environments dirty by generating comtypes/gen + folder. Fix is to ignore that folder. +- Added some more diagnostics information. + ## v9.3.9 (date: 8.2.2021) - micromamba cleanup bug fix (got error if micromamba is missing) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 6532ef52..c2ff2be3 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -6,8 +6,10 @@ import ( "io" "net" "os" + "os/user" "runtime" "sort" + "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -86,6 +88,12 @@ func RunDiagnostics() *DiagnosticStatus { result.Details["installationId"] = xviper.TrackingIdentity() result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) + result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") + + who, err := user.Current() + if err == nil { + result.Details["uid:gid"] = fmt.Sprintf("%s:%s", who.Uid, who.Gid) + } // checks result.Checks = append(result.Checks, robocorpHomeCheck()) From bea0ff526d9cc4e6b9bda22d6f59c461684a5791 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 15 Feb 2021 11:46:56 +0200 Subject: [PATCH 067/516] UPGRADE: new micromamba version 0.7.14 (v9.3.11) - micromamba upgrade to 0.7.14 - made process fail early and visibly, if micromamba download fails --- common/logger.go | 7 +++++++ common/version.go | 2 +- conda/download.go | 5 +++++ conda/installing.go | 11 +++++++---- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- conda/workflows.go | 4 ++-- docs/changelog.md | 5 +++++ 10 files changed, 31 insertions(+), 11 deletions(-) diff --git a/common/logger.go b/common/logger.go index 098b3efc..a6ebd268 100644 --- a/common/logger.go +++ b/common/logger.go @@ -15,6 +15,13 @@ func printout(out io.Writer, message string) { fmt.Fprintf(out, "%s%s\n", stamp, message) } +func Fatal(context string, err error) { + if err != nil { + printout(os.Stderr, fmt.Sprintf("Fatal [%s]: %v", context, err)) + os.Stderr.Sync() + } +} + func Error(context string, err error) { if err != nil { Log("Error [%s]: %v", context, err) diff --git a/common/version.go b/common/version.go index 0034ccf7..8f34f018 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.10` + Version = `v9.3.11` ) diff --git a/conda/download.go b/conda/download.go index 4c3be77e..5caa263d 100644 --- a/conda/download.go +++ b/conda/download.go @@ -2,6 +2,7 @@ package conda import ( "crypto/sha256" + "fmt" "io" "net/http" "os" @@ -20,6 +21,10 @@ func DownloadMicromamba() error { } defer response.Body.Close() + if response.StatusCode < 200 || response.StatusCode >= 300 { + return fmt.Errorf("Downloading %q failed, reason: %q!", url, response.Status) + } + if pathlib.Exists(BinMicromamba()) { os.Remove(BinMicromamba()) } diff --git a/conda/installing.go b/conda/installing.go index 0a1ce03a..4d2dfa9f 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -2,25 +2,28 @@ package conda import ( "os" + "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" ) func MustMicromamba() bool { - return HasMicroMamba() || ((DoDownload() || DoDownload() || DoDownload()) && DoInstall()) + return HasMicroMamba() || ((DoDownload(1*time.Millisecond) || DoDownload(1*time.Second) || DoDownload(3*time.Second)) && DoInstall()) } -func DoDownload() bool { +func DoDownload(delay time.Duration) bool { if common.DebugFlag { defer common.Stopwatch("Download done in").Report() } common.Log("Downloading micromamba, this may take awhile ...") + time.Sleep(delay) + err := DownloadMicromamba() if err != nil { - common.Error("Download", err) + common.Fatal("Download", err) os.Remove(BinMicromamba()) return false } @@ -37,7 +40,7 @@ func DoInstall() bool { err := os.Chmod(BinMicromamba(), 0o755) if err != nil { - common.Error("Install", err) + common.Fatal("Install", err) return false } cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.install", common.Version) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 49e73eb9..b12664b2 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.12/macos64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.7.14/macos64/micromamba" } func IsPosix() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index ee67bafb..5ceb975c 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -18,7 +18,7 @@ var ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.12/linux64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.7.14/linux64/micromamba" } func ExpandPath(entry string) string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index e787416f..6c91ceef 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -24,7 +24,7 @@ const ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.12/windows64/micromamba.exe" + return "https://downloads.robocorp.com/micromamba/v0.7.14/windows64/micromamba.exe" } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index bbb276a0..8a865f99 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -235,7 +235,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 7012 + goodEnough := version >= 7014 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/conda/workflows.go b/conda/workflows.go index 60e8b752..10f465a3 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -150,9 +150,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - command := []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} + command := []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} if common.DebugFlag { - command = []string{BinMicromamba(), "create", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- conda env create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 4d31e2bf..719f600b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.3.11 (date: 15.2.2021) + +- micromamba upgrade to 0.7.14 +- made process fail early and visibly, if micromamba download fails + ## v9.3.10 (date: 11.2.2021) - Windows automation made environments dirty by generating comtypes/gen From 80417b4758b1d39044f0704a4afda44e05d8eddc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Feb 2021 09:05:09 +0200 Subject: [PATCH 068/516] GH#12: temp recycling delay (v9.3.12) - introduced 48 hour delay to recycling temp folders (since clients depend on having temp around after rcc process is gone); this closes #12 --- cmd/rcc/main.go | 8 +++++++- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 986d5bd1..5264852e 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -4,11 +4,13 @@ import ( "io/ioutil" "os" "path/filepath" + "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pathlib" ) var ( @@ -40,7 +42,11 @@ func startTempRecycling() { return } for _, filename := range found { - go os.RemoveAll(filepath.Dir(filename)) + folder := filepath.Dir(filename) + changed, err := pathlib.Modtime(folder) + if err == nil && time.Since(changed) > 48*time.Hour { + go os.RemoveAll(folder) + } } } diff --git a/common/version.go b/common/version.go index 8f34f018..079fe61f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.11` + Version = `v9.3.12` ) diff --git a/docs/changelog.md b/docs/changelog.md index 719f600b..b4d34d1e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.3.12 (date: 17.2.2021) + +- introduced 48 hour delay to recycling temp folders (since clients depend on + having temp around after rcc process is gone); this closes #12 + ## v9.3.11 (date: 15.2.2021) - micromamba upgrade to 0.7.14 From e80b2e9fb88e8413a47223b93d8e15306603a9ed Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Feb 2021 13:54:35 +0200 Subject: [PATCH 069/516] RCC-55: robot validation command (v9.4.0) - added initial robot diagnostics (just robot.yaml for now) - integrated robot diagnostics into configuration diagnostics (optional) - integrated robot diagnostics to issue reporting (optional) - fix: windows paths were wrong; "bin" to "usr" change --- cmd/configure.go | 4 +- cmd/diagnostics.go | 6 +- cmd/issue.go | 4 +- cmd/robotdiagnostics.go | 31 ++++++++++ cmd/run.go | 2 +- cmd/shell.go | 2 +- cmd/testrun.go | 2 +- cmd/variables.go | 4 +- common/diagnostics.go | 61 +++++++++++++++++++ common/version.go | 2 +- conda/platform_windows_amd64.go | 2 +- docs/changelog.md | 7 +++ operations/diagnostics.go | 94 ++++++++++++++++------------- operations/fixing.go | 4 -- operations/issues.go | 8 +-- robot/robot.go | 103 ++++++++++++++++++++++++++++++++ 16 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 cmd/robotdiagnostics.go create mode 100644 common/diagnostics.go diff --git a/cmd/configure.go b/cmd/configure.go index 39e14e44..ba1bb655 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -5,8 +5,8 @@ import ( ) var configureCmd = &cobra.Command{ - Use: "configure", - Aliases: []string{"conf", "config"}, + Use: "configuration", + Aliases: []string{"conf", "config", "configure"}, Short: "Group of commands related to `rcc configuration`.", Long: "Group of commands to configure rcc with your settings.", } diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index b82b05c1..c886c7c1 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -9,7 +9,8 @@ import ( ) var ( - fileOption string + fileOption string + robotOption string ) var diagnosticsCmd = &cobra.Command{ @@ -20,7 +21,7 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - err := operations.PrintDiagnostics(fileOption, jsonFlag) + err := operations.PrintDiagnostics(fileOption, robotOption, jsonFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -32,4 +33,5 @@ func init() { configureCmd.AddCommand(diagnosticsCmd) diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") diagnosticsCmd.Flags().StringVarP(&fileOption, "file", "f", "", "Save output into a file.") + diagnosticsCmd.Flags().StringVarP(&robotOption, "robot", "r", "", "Full path to 'robot.yaml' configuration file. [optional]") } diff --git a/cmd/issue.go b/cmd/issue.go index 7db9e4b0..163f8374 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -9,6 +9,7 @@ import ( ) var ( + issueRobot string issueMetafile string issueAttachments []string ) @@ -21,7 +22,7 @@ var issueCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Feedback issue lasted").Report() } - err := operations.ReportIssue(issueMetafile, issueAttachments, dryFlag) + err := operations.ReportIssue(issueRobot, issueMetafile, issueAttachments, dryFlag) if err != nil { pretty.Exit(1, "Error: %s", err) } @@ -34,4 +35,5 @@ func init() { issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") issueCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't send issue report, just show what would report be.") + issueCmd.Flags().StringVarP(&issueRobot, "robot", "", "", "Full path to 'robot.yaml' configuration file. [optional]") } diff --git a/cmd/robotdiagnostics.go b/cmd/robotdiagnostics.go new file mode 100644 index 00000000..747daa09 --- /dev/null +++ b/cmd/robotdiagnostics.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var robotDiagnosticsCmd = &cobra.Command{ + Use: "diagnostics", + Short: "Run system diagnostics to help resolve rcc issues.", + Long: "Run system diagnostics to help resolve rcc issues.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Diagnostic run lasted").Report() + } + err := operations.PrintRobotDiagnostics(robotFile, jsonFlag) + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + pretty.Ok() + }, +} + +func init() { + robotCmd.AddCommand(robotDiagnosticsCmd) + robotDiagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + robotDiagnosticsCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") +} diff --git a/cmd/run.go b/cmd/run.go index 9df0c692..3f9fe59e 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -53,7 +53,7 @@ func init() { rootCmd.AddCommand(runCmd) runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") - runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file. (Backward compatibility with 'package.yaml')") + runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") diff --git a/cmd/shell.go b/cmd/shell.go index d813902b..1763acd0 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -37,7 +37,7 @@ func init() { taskCmd.AddCommand(shellCmd) shellCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") - shellCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file. (With backward compatibility with 'package.yaml')") + shellCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") shellCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to configure shell from configuration file.") shellCmd.MarkFlagRequired("config") } diff --git a/cmd/testrun.go b/cmd/testrun.go index 35859b61..0f229e20 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -87,7 +87,7 @@ func init() { testrunCmd.Flags().StringArrayVarP(&ignores, "ignore", "i", []string{}, "File with ignore patterns.") testrunCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") - testrunCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file. (Backward compatibility with 'package.yaml')") + testrunCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") testrunCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file.") testrunCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") testrunCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") diff --git a/cmd/variables.go b/cmd/variables.go index 45d45af8..fb700115 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -74,7 +74,7 @@ func exportEnvironment(condaYaml []string, packfile, taskName, environment, work } if len(condaYaml) < 1 { - return errors.New("No robot.yaml, package.yaml or conda.yaml files given. Cannot continue.") + return errors.New("No robot.yaml, or conda.yaml files given. Cannot continue.") } label, err := conda.NewEnvironment(forceFlag, condaYaml...) @@ -144,7 +144,7 @@ func init() { envCmd.AddCommand(variablesCmd) variablesCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to 'env.json' development environment data file. ") - variablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. (Backward compatibility with 'package.yaml') ") + variablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") variablesCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file. ") variablesCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. ") variablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") diff --git a/common/diagnostics.go b/common/diagnostics.go new file mode 100644 index 00000000..9d19ab82 --- /dev/null +++ b/common/diagnostics.go @@ -0,0 +1,61 @@ +package common + +import ( + "encoding/json" + "fmt" +) + +const ( + StatusOk = `ok` + StatusWarning = `warning` + StatusFail = `fail` + StatusFatal = `fatal` +) + +type Diagnoser func(status, link, form string, details ...interface{}) + +func (it Diagnoser) Ok(form string, details ...interface{}) { + it(StatusOk, "", form, details...) +} + +func (it Diagnoser) Warning(link, form string, details ...interface{}) { + it(StatusWarning, link, form, details...) +} + +func (it Diagnoser) Fail(link, form string, details ...interface{}) { + it(StatusFail, link, form, details...) +} + +func (it Diagnoser) Fatal(link, form string, details ...interface{}) { + it(StatusFatal, link, form, details...) +} + +type DiagnosticStatus struct { + Details map[string]string `json:"details"` + Checks []*DiagnosticCheck `json:"checks"` +} + +type DiagnosticCheck struct { + Type string `json:"type"` + Status string `json:"status"` + Message string `json:"message"` + Link string `json:"url"` +} + +func (it *DiagnosticStatus) check(kind, status, message, link string) { + it.Checks = append(it.Checks, &DiagnosticCheck{kind, status, message, link}) +} + +func (it *DiagnosticStatus) Diagnose(kind string) Diagnoser { + return func(status, link, form string, details ...interface{}) { + it.check(kind, status, fmt.Sprintf(form, details...), link) + } +} + +func (it *DiagnosticStatus) AsJson() (string, error) { + body, err := json.MarshalIndent(it, "", " ") + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/common/version.go b/common/version.go index 079fe61f..db142f74 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.3.12` + Version = `v9.4.0` ) diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 6c91ceef..db2d4a1f 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -19,7 +19,7 @@ const ( defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" librarySuffix = "\\Library" scriptSuffix = "\\Scripts" - usrSuffix = "\\bin" + usrSuffix = "\\usr" binSuffix = "\\bin" ) diff --git a/docs/changelog.md b/docs/changelog.md index b4d34d1e..332401fa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.4.0 (date: 17.2.2021) + +- added initial robot diagnostics (just robot.yaml for now) +- integrated robot diagnostics into configuration diagnostics (optional) +- integrated robot diagnostics to issue reporting (optional) +- fix: windows paths were wrong; "bin" to "usr" change + ## v9.3.12 (date: 17.2.2021) - introduced 48 hour delay to recycling temp folders (since clients depend on diff --git a/operations/diagnostics.go b/operations/diagnostics.go index c2ff2be3..1638b522 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -1,7 +1,6 @@ package operations import ( - "encoding/json" "fmt" "io" "net" @@ -15,6 +14,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" "github.com/robocorp/rcc/xviper" ) @@ -41,26 +41,6 @@ var ( } ) -type DiagnosticStatus struct { - Details map[string]string `json:"details"` - Checks []*DiagnosticCheck `json:"checks"` -} - -type DiagnosticCheck struct { - Type string `json:"type"` - Status string `json:"status"` - Message string `json:"message"` - Link string `json:"url"` -} - -func (it *DiagnosticStatus) AsJson() (string, error) { - body, err := json.MarshalIndent(it, "", " ") - if err != nil { - return "", err - } - return string(body), nil -} - type stringerr func() (string, error) func justText(source stringerr) string { @@ -68,10 +48,10 @@ func justText(source stringerr) string { return result } -func RunDiagnostics() *DiagnosticStatus { - result := &DiagnosticStatus{ +func RunDiagnostics() *common.DiagnosticStatus { + result := &common.DiagnosticStatus{ Details: make(map[string]string), - Checks: []*DiagnosticCheck{}, + Checks: []*common.DiagnosticCheck{}, } executable, _ := os.Executable() result.Details["executable"] = executable @@ -116,16 +96,16 @@ func rccStatusLine() string { return fmt.Sprintf("%d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s", templates, requests, merges, hits, dirty, misses, failures, xviper.TrackingIdentity()) } -func longPathSupportCheck() *DiagnosticCheck { +func longPathSupportCheck() *common.DiagnosticCheck { if conda.HasLongPathSupport() { - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "OS", Status: statusOk, Message: "Supports long enough paths.", Link: supportLongPathUrl, } } - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "OS", Status: statusFail, Message: "Does not support long path names!", @@ -133,16 +113,16 @@ func longPathSupportCheck() *DiagnosticCheck { } } -func robocorpHomeCheck() *DiagnosticCheck { +func robocorpHomeCheck() *common.DiagnosticCheck { if !conda.ValidLocation(conda.RobocorpHome()) { - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "RPA", Status: statusFatal, Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", conda.RobocorpHome()), Link: supportGeneralUrl, } } - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "RPA", Status: statusOk, Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", conda.RobocorpHome()), @@ -150,17 +130,17 @@ func robocorpHomeCheck() *DiagnosticCheck { } } -func dnsLookupCheck(site string) *DiagnosticCheck { +func dnsLookupCheck(site string) *common.DiagnosticCheck { found, err := net.LookupHost(site) if err != nil { - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "network", Status: statusFail, Message: fmt.Sprintf("DNS lookup %s failed: %v", site, err), Link: supportNetworkUrl, } } - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "network", Status: statusOk, Message: fmt.Sprintf("%s found: %v", site, found), @@ -168,10 +148,10 @@ func dnsLookupCheck(site string) *DiagnosticCheck { } } -func canaryDownloadCheck() *DiagnosticCheck { +func canaryDownloadCheck() *common.DiagnosticCheck { client, err := cloud.NewClient(canaryHost) if err != nil { - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "network", Status: statusFail, Message: fmt.Sprintf("%v: %v", canaryHost, err), @@ -181,14 +161,14 @@ func canaryDownloadCheck() *DiagnosticCheck { request := client.NewRequest(canaryUrl) response := client.Get(request) if response.Status != 200 || string(response.Body) != "Used to testing connections" { - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "network", Status: statusFail, Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), Link: supportNetworkUrl, } } - return &DiagnosticCheck{ + return &common.DiagnosticCheck{ Type: "network", Status: statusOk, Message: fmt.Sprintf("Canary download successful: %s%s", canaryHost, canaryUrl), @@ -196,7 +176,7 @@ func canaryDownloadCheck() *DiagnosticCheck { } } -func jsonDiagnostics(sink io.Writer, details *DiagnosticStatus) { +func jsonDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { form, err := details.AsJson() if err != nil { pretty.Exit(1, "Error: %s", err) @@ -204,7 +184,7 @@ func jsonDiagnostics(sink io.Writer, details *DiagnosticStatus) { fmt.Fprintln(sink, form) } -func humaneDiagnostics(sink io.Writer, details *DiagnosticStatus) { +func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { fmt.Fprintln(sink, "Diagnostics:") keys := make([]string, 0, len(details.Details)) for key, _ := range details.Details { @@ -213,7 +193,7 @@ func humaneDiagnostics(sink io.Writer, details *DiagnosticStatus) { sort.Strings(keys) for _, key := range keys { value := details.Details[key] - fmt.Fprintf(sink, " - %-18s... %q\n", key, value) + fmt.Fprintf(sink, " - %-25s... %q\n", key, value) } fmt.Fprintln(sink, "") fmt.Fprintln(sink, "Checks:") @@ -233,13 +213,16 @@ func fileIt(filename string) (io.WriteCloser, error) { return file, nil } -func PrintDiagnostics(filename string, json bool) error { +func PrintDiagnostics(filename, robotfile string, json bool) error { file, err := fileIt(filename) if err != nil { return err } defer file.Close() result := RunDiagnostics() + if len(robotfile) > 0 { + addRobotDiagnostics(robotfile, result) + } if json { jsonDiagnostics(file, result) } else { @@ -247,3 +230,32 @@ func PrintDiagnostics(filename string, json bool) error { } return nil } + +func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus) { + config, err := robot.LoadRobotYaml(robotfile, false) + diagnose := target.Diagnose("Robot") + if err != nil { + diagnose.Fail(supportGeneralUrl, "About robot.yaml: %v", err) + } else { + config.Diagnostics(target) + } +} + +func RunRobotDiagnostics(robotfile string) *common.DiagnosticStatus { + result := &common.DiagnosticStatus{ + Details: make(map[string]string), + Checks: []*common.DiagnosticCheck{}, + } + addRobotDiagnostics(robotfile, result) + return result +} + +func PrintRobotDiagnostics(robotfile string, json bool) error { + result := RunRobotDiagnostics(robotfile) + if json { + jsonDiagnostics(os.Stdout, result) + } else { + humaneDiagnostics(os.Stderr, result) + } + return nil +} diff --git a/operations/fixing.go b/operations/fixing.go index 3e614f22..f3587192 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -90,9 +90,5 @@ func FixDirectory(dir string) error { if pathlib.IsFile(primary) { return FixRobot(primary) } - secondary := filepath.Join(dir, "package.yaml") - if pathlib.IsFile(secondary) { - return FixRobot(secondary) - } return nil } diff --git a/operations/issues.go b/operations/issues.go index f7e1b9be..ae9de567 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -47,9 +47,9 @@ func createIssueZip(attachmentsFiles []string) (string, error) { return zipfile, nil } -func createDiagnosticsReport() (string, error) { +func createDiagnosticsReport(robotfile string) (string, error) { file := filepath.Join(conda.RobocorpTemp(), "diagnostics.txt") - err := PrintDiagnostics(file, false) + err := PrintDiagnostics(file, robotfile, false) if err != nil { return "", err } @@ -64,13 +64,13 @@ func virtualName(filename string) (string, error) { return fmt.Sprintf("attachments_%s.zip", digest[:16]), nil } -func ReportIssue(reportFile string, attachmentsFiles []string, dryrun bool) error { +func ReportIssue(robotFile, reportFile string, attachmentsFiles []string, dryrun bool) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) token, err := loadToken(reportFile) if err != nil { return err } - diagnostics, err := createDiagnosticsReport() + diagnostics, err := createDiagnosticsReport(robotFile) if err == nil { attachmentsFiles = append(attachmentsFiles, diagnostics) } diff --git a/robot/robot.go b/robot/robot.go index bbeb7310..641c409f 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -27,6 +27,7 @@ type Robot interface { CondaConfigFile() string RootDirectory() string Validate() (bool, error) + Diagnostics(*common.DiagnosticStatus) WorkingDirectory() string ArtifactDirectory() string @@ -62,6 +63,108 @@ type task struct { Command []string `yaml:"command,omitempty"` } +func (it *robot) diagnoseTasks(diagnose common.Diagnoser) { + if it.Tasks == nil { + diagnose.Fail("", "Missing 'tasks:' from robot.yaml.") + return + } + ok := true + if len(it.Tasks) == 0 { + diagnose.Fail("", "There must be at least one task defined in 'tasks:' section in robot.yaml.") + ok = false + } else { + diagnose.Ok("Tasks are defined in robot.yaml") + } + for name, task := range it.Tasks { + count := 0 + if len(task.Task) > 0 { + count += 1 + } + if len(task.Shell) > 0 { + count += 1 + } + if task.Command != nil && len(task.Command) > 0 { + count += 1 + } + if count != 1 { + diagnose.Fail("", "In robot.yaml, task '%s' needs exactly one of robotTaskName/shell/command definition!", name) + ok = false + } + } + if ok { + diagnose.Ok("Each task has exactly one definition.") + } +} + +func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { + ok := true + for _, path := range it.Path { + if filepath.IsAbs(path) { + diagnose.Fail("", "PATH entry %q seems to be absolute, which makes robot machine dependent.", path) + ok = false + } + } + if ok { + diagnose.Ok("PATH settings in robot.yaml are ok.") + } + ok = true + for _, path := range it.Pythonpath { + if filepath.IsAbs(path) { + diagnose.Fail("", "PYTHONPATH entry %q seems to be absolute, which makes robot machine dependent.", path) + ok = false + } + } + if ok { + diagnose.Ok("PYTHONPATH settings in robot.yaml are ok.") + } + ok = true + if it.Ignored == nil || len(it.Ignored) == 0 { + diagnose.Warning("", "No ignoreFiles defined, so everything ends up inside robot.zip file.") + ok = false + } else { + for _, path := range it.Ignored { + if filepath.IsAbs(path) { + diagnose.Fail("", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) + ok = false + } + } + } + if ok { + diagnose.Ok("ignoreFiles settings in robot.yaml are ok.") + } +} + +func (it *robot) Diagnostics(target *common.DiagnosticStatus) { + diagnose := target.Diagnose("Robot") + it.diagnoseTasks(diagnose) + it.diagnoseVariousPaths(diagnose) + if it.Artifacts == "" { + diagnose.Fail("", "In robot.yaml, 'artifactsDir:' is required!") + } else { + if filepath.IsAbs(it.Artifacts) { + diagnose.Fail("", "artifactDir %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) + } else { + diagnose.Ok("Artifacts directory defined in robot.yaml") + } + } + if it.Conda == "" { + diagnose.Ok("In robot.yaml, 'condaConfigFile:' is missing. So this is shell robot.") + } else { + if filepath.IsAbs(it.Conda) { + diagnose.Fail("", "condaConfigFile %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) + } else { + diagnose.Ok("In robot.yaml, 'condaConfigFile:' is present. So this is python robot.") + } + } + target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) + target.Details["robot-conda-file"] = it.CondaConfigFile() + target.Details["robot-root-directory"] = it.RootDirectory() + target.Details["robot-working-directory"] = it.WorkingDirectory() + target.Details["robot-artifact-directory"] = it.ArtifactDirectory() + target.Details["robot-paths"] = strings.Join(it.Paths(), ", ") + target.Details["robot-python-paths"] = strings.Join(it.PythonPaths(), ", ") +} + func (it *robot) Validate() (bool, error) { if it.Tasks == nil { return false, errors.New("In robot.yaml, 'tasks:' is required!") From 57e42c52c08e9c31c14d99e8a2b4adfc8a25814a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 18 Feb 2021 13:12:03 +0200 Subject: [PATCH 070/516] RCC-55: conda.yaml diagnostics (v9.4.1) - added conda.yaml diagnostics (initial take) - made `rcc env variables` to be not silent anymore - log level changes in environment creation - env creation workflow has now 6 steps, added identity visibility --- cmd/variables.go | 7 ----- common/version.go | 2 +- conda/condayaml.go | 62 +++++++++++++++++++++++++++++++++++++++ conda/workflows.go | 29 ++++++++++-------- docs/changelog.md | 7 +++++ robot/robot.go | 7 +++++ robot_tests/fullrun.robot | 18 +++++++----- 7 files changed, 103 insertions(+), 29 deletions(-) diff --git a/cmd/variables.go b/cmd/variables.go index fb700115..7f1ad438 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -122,13 +122,6 @@ var variablesCmd = &cobra.Command{ Short: "Export environment specific variables as a JSON structure.", Long: "Export environment specific variables as a JSON structure.", Run: func(cmd *cobra.Command, args []string) { - silent := common.Silent - common.Silent = true - - defer func() { - common.Silent = silent - }() - ok := conda.MustMicromamba() if !ok { pretty.Exit(2, "Could not get micromamba installed.") diff --git a/common/version.go b/common/version.go index db142f74..3afd6cb0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.4.0` + Version = `v9.4.1` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 395a3792..27781ec0 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -371,6 +371,68 @@ func (it *Environment) AsRequirementsText() string { return strings.Join(lines, Newline) } +func (it *Environment) Diagnostics(target *common.DiagnosticStatus) { + diagnose := target.Diagnose("Conda") + countChannels := len(it.Channels) + defaultsPostion := -1 + floating := false + ok := true + for index, channel := range it.Channels { + if channel == "defaults" { + defaultsPostion = index + diagnose.Warning("", "Try to avoid defaults channel, and prefer using conda-forge instead.") + ok = false + } + } + if defaultsPostion == 0 && countChannels > 1 { + diagnose.Warning("", "Try to avoid putting defaults channel as first channel.") + ok = false + } + if ok { + diagnose.Ok("Channels in conda.yaml are ok.") + } + ok = true + for _, dependency := range it.Conda { + if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { + diagnose.Warning("", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + ok = false + floating = true + } + if len(dependency.Qualifier) > 0 && !(dependency.Qualifier == "==" || dependency.Qualifier == "=") { + diagnose.Fail("", "Conda dependency %q must use '==' or '=' for version declaration.", dependency.Original) + ok = false + floating = true + } + } + if ok { + diagnose.Ok("Conda dependencies in conda.yaml are ok.") + } + ok = true + pipCount := len(it.Pip) + if pipCount > 0 { + diagnose.Warning("", "There is %d pip dependencies. Please, prefer using conda dependencies over pip dependencies.", pipCount) + ok = false + } + for _, dependency := range it.Pip { + if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { + diagnose.Warning("", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + ok = false + floating = true + } + if len(dependency.Qualifier) > 0 && dependency.Qualifier != "==" { + diagnose.Fail("", "Pip dependency %q must use '==' for version declaration.", dependency.Original) + ok = false + floating = true + } + } + if ok { + diagnose.Ok("Pip dependencies in conda.yaml are ok.") + } + if floating { + diagnose.Warning("", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") + } +} + func CondaYamlFrom(content []byte) (*Environment, error) { result := new(internalEnvironment) err := yaml.Unmarshal(content, result) diff --git a/conda/workflows.go b/conda/workflows.go index 10f465a3..9822af2a 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -155,10 +155,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh command = []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) - common.Debug("=== new live --- conda env create phase ===") + common.Debug("=== new live --- micromamba create phase ===") code, err := shell.New(CondaEnvironment(), ".", command...).StderrOnly().Observed(observer, false) if err != nil || code != 0 { - common.Error("Conda error", err) + common.Fatal("Micromamba", err) return false, false } if observer.HasFailures(targetFolder) { @@ -167,9 +167,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCache, wheelCache := PipCache(), WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - common.Log("#### Progress: 3/5 [pip install phase skipped -- no pip dependencies]") + common.Log("#### Progress: 4/6 [pip install phase skipped -- no pip dependencies]") } else { - common.Log("#### Progress: 3/5 [pip install phase]") + common.Log("#### Progress: 4/6 [pip install phase]") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet"} if common.DebugFlag { @@ -178,7 +178,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== new live --- pip install phase ===") err = LiveExecution(targetFolder, pipCommand...) if err != nil { - common.Error("Pip error", err) + common.Fatal("Pip", err) return false, false } } @@ -187,12 +187,14 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh for _, script := range postInstall { scriptCommand, err := shlex.Split(script) if err != nil { + common.Fatal("post-install", err) common.Log("%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) return false, false } common.Log("Running post install script '%s' ...", script) err = LiveExecution(targetFolder, scriptCommand...) if err != nil { + common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) return false, false } @@ -208,7 +210,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh digest, err := DigestFor(targetFolder) if err != nil { - common.Error("Digest", err) + common.Fatal("Digest", err) return false, false } return metaSave(targetFolder, Hexdigest(digest)) == nil, false @@ -288,9 +290,9 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { defer func() { templates = len(TemplateList()) - common.Log("#### Progress: 5/5 [Done.] [Cache statistics: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) + common.Log("#### Progress: 6/6 [Done.] [Cache statistics: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) }() - common.Log("#### Progress: 0/5 [try use existing live same environment?] %v", xviper.TrackingIdentity()) + common.Log("#### Progress: 0/6 [try use existing live same environment?] %v", xviper.TrackingIdentity()) xviper.Set("stats.env.request", requests) @@ -311,6 +313,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } defer os.Remove(condaYaml) defer os.Remove(requirementsText) + common.Log("#### Progress: 1/6 [environment key is: %s]", key) common.EnvironmentHash = key @@ -333,9 +336,9 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } if common.Stageonly { - common.Log("#### Progress: 1/5 [skipped -- stage only]") + common.Log("#### Progress: 2/6 [skipped -- stage only]") } else { - common.Log("#### Progress: 1/5 [try clone existing same template to live, key: %v]", key) + common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) success, err := CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) if err != nil { return "", err @@ -346,7 +349,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return liveFolder, nil } } - common.Log("#### Progress: 2/5 [try create new environment from scratch]") + common.Log("#### Progress: 3/6 [try create new environment from scratch]") success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) if err != nil { return "", err @@ -355,9 +358,9 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { misses += 1 xviper.Set("stats.env.miss", misses) if common.Liveonly { - common.Log("#### Progress: 4/5 [skipped -- live only]") + common.Log("#### Progress: 5/6 [skipped -- live only]") } else { - common.Log("#### Progress: 4/5 [backup new environment as template]") + common.Log("#### Progress: 5/6 [backup new environment as template]") _, err = CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) if err != nil { return "", err diff --git a/docs/changelog.md b/docs/changelog.md index 332401fa..7a580a62 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.4.1 (date: 17.2.2021) + +- added conda.yaml diagnostics (initial take) +- made `rcc env variables` to be not silent anymore +- log level changes in environment creation +- env creation workflow has now 6 steps, added identity visibility + ## v9.4.0 (date: 17.2.2021) - added initial robot diagnostics (just robot.yaml for now) diff --git a/robot/robot.go b/robot/robot.go index 641c409f..4342d306 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -154,6 +154,12 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus) { diagnose.Fail("", "condaConfigFile %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) } else { diagnose.Ok("In robot.yaml, 'condaConfigFile:' is present. So this is python robot.") + condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) + if err != nil { + diagnose.Fail("", "From robot.yaml, loading conda.yaml failed with: %v", err) + } else { + condaEnv.Diagnostics(target) + } } } target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) @@ -163,6 +169,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus) { target.Details["robot-artifact-directory"] = it.ArtifactDirectory() target.Details["robot-paths"] = strings.Join(it.Paths(), ", ") target.Details["robot-python-paths"] = strings.Join(it.PythonPaths(), ", ") + } func (it *robot) Validate() (bool, error) { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 913dfb8a..2f0af3e9 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -78,8 +78,9 @@ Using and running template example with shell file Step build/rcc task run --controller citests -r tmp/fluffy/robot.yaml Must Have 1 critical task, 1 passed, 0 failed Use STDERR - Must Have Progress: 0/5 - Must Have Progress: 5/5 + Must Have Progress: 0/6 + Must Have Progress: 1/6 + Must Have Progress: 6/6 Must Have rpaframework Must Have OK. Must Exist %{ROBOCORP_HOME}/base/ @@ -92,12 +93,13 @@ Using and running template example with shell file Must Have 1 critical task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 0/5 - Wont Have Progress: 1/5 - Wont Have Progress: 2/5 - Wont Have Progress: 3/5 - Wont Have Progress: 4/5 - Must Have Progress: 5/5 + Must Have Progress: 0/6 + Must Have Progress: 1/6 + Wont Have Progress: 2/6 + Wont Have Progress: 3/6 + Wont Have Progress: 4/6 + Wont Have Progress: 5/6 + Must Have Progress: 6/6 Must Have OK. Goal Merge two different conda.yaml files with conflict fails From b7e7fc0085e9d4b3aca90b988829f3983361e15c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 23 Feb 2021 08:50:53 +0200 Subject: [PATCH 071/516] RCC-152: issue reporting bug fix (v9.4.2) - fix: marked --report flag required in issue reporting - added account-email to issue report, as backup contact information --- cmd/issue.go | 11 ++++++++++- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/issues.go | 3 ++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/issue.go b/cmd/issue.go index 163f8374..ef377c7f 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -22,7 +22,15 @@ var issueCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Feedback issue lasted").Report() } - err := operations.ReportIssue(issueRobot, issueMetafile, issueAttachments, dryFlag) + accountEmail := "unknown" + account := operations.AccountByName(AccountName()) + if account != nil && account.Details != nil { + email, ok := account.Details["email"].(string) + if ok { + accountEmail = email + } + } + err := operations.ReportIssue(accountEmail, issueRobot, issueMetafile, issueAttachments, dryFlag) if err != nil { pretty.Exit(1, "Error: %s", err) } @@ -33,6 +41,7 @@ var issueCmd = &cobra.Command{ func init() { feedbackCmd.AddCommand(issueCmd) issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") + issueCmd.MarkFlagRequired("report") issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") issueCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't send issue report, just show what would report be.") issueCmd.Flags().StringVarP(&issueRobot, "robot", "", "", "Full path to 'robot.yaml' configuration file. [optional]") diff --git a/common/version.go b/common/version.go index 3afd6cb0..b0732d91 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.4.1` + Version = `v9.4.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7a580a62..6b7a62e8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.4.2 (date: 23.2.2021) + +- fix: marked --report flag required in issue reporting +- added account-email to issue report, as backup contact information + ## v9.4.1 (date: 17.2.2021) - added conda.yaml diagnostics (initial take) diff --git a/operations/issues.go b/operations/issues.go index ae9de567..072430fe 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -64,7 +64,7 @@ func virtualName(filename string) (string, error) { return fmt.Sprintf("attachments_%s.zip", digest[:16]), nil } -func ReportIssue(robotFile, reportFile string, attachmentsFiles []string, dryrun bool) error { +func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, dryrun bool) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) token, err := loadToken(reportFile) if err != nil { @@ -85,6 +85,7 @@ func ReportIssue(robotFile, reportFile string, attachmentsFiles []string, dryrun } installationId := xviper.TrackingIdentity() token["installationId"] = installationId + token["account-email"] = email token["fileName"] = shortname token["controller"] = common.ControllerIdentity() _, ok := token["platform"] From fb379336381f50957c1b38f33721de3739123c1b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 23 Feb 2021 17:42:33 +0200 Subject: [PATCH 072/516] RCC-55: generic json/yaml diagnostics (v9.4.3) - added generic reading and parsing diagnostics for JSON and YAML files --- common/version.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 4 ++++ go.mod | 1 + go.sum | 2 ++ operations/diagnostics.go | 43 +++++++++++++++++++++++++++++++++++++++ pathlib/walk.go | 13 ++++++++++++ 7 files changed, 65 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index b0732d91..b909dcc3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.4.2` + Version = `v9.4.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index 9822af2a..b7ea7181 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -203,7 +203,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== new live --- finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") - err = ioutil.WriteFile(markerFile, []byte(yaml), 0o640) + err = ioutil.WriteFile(markerFile, []byte(yaml), 0o644) if err != nil { return false, false } diff --git a/docs/changelog.md b/docs/changelog.md index 6b7a62e8..cb6b5583 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.4.3 (date: 23.2.2021) + +- added generic reading and parsing diagnostics for JSON and YAML files + ## v9.4.2 (date: 23.2.2021) - fix: marked --report flag required in issue reporting diff --git a/go.mod b/go.mod index 76d838fc..f0ab850b 100644 --- a/go.mod +++ b/go.mod @@ -18,5 +18,6 @@ require ( golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.55.0 // indirect + gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index a2eb5720..4b537b69 100644 --- a/go.sum +++ b/go.sum @@ -188,6 +188,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 1638b522..d4ad8f95 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -1,21 +1,27 @@ package operations import ( + "encoding/json" "fmt" "io" + "io/ioutil" "net" "os" "os/user" + "path/filepath" "runtime" "sort" + "strings" "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" "github.com/robocorp/rcc/xviper" + "gopkg.in/yaml.v1" ) const ( @@ -231,6 +237,42 @@ func PrintDiagnostics(filename, robotfile string, json bool) error { return nil } +type Unmarshaler func([]byte, interface{}) error + +func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []string, target *common.DiagnosticStatus) { + target.Details[fmt.Sprintf("%s-file-count", strings.ToLower(label))] = fmt.Sprintf("%d file(s)", len(paths)) + diagnose := target.Diagnose(label) + var canary interface{} + success := true + investigated := false + for _, tail := range paths { + investigated = true + fullpath := filepath.Join(rootdir, tail) + content, err := ioutil.ReadFile(fullpath) + if err != nil { + diagnose.Fail(supportGeneralUrl, "Problem reading %s file %q: %v", label, tail, err) + success = false + continue + } + err = tool(content, &canary) + if err != nil { + diagnose.Fail(supportGeneralUrl, "Problem parsing %s file %q: %v", label, tail, err) + success = false + } + } + if investigated && success { + diagnose.Ok("%s files are readable and can be parsed.", label) + } +} + +func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { + jsons := pathlib.Glob(rootdir, "*.json") + diagnoseFilesUnmarshal(json.Unmarshal, "JSON", rootdir, jsons, target) + yamls := pathlib.Glob(rootdir, "*.yaml") + yamls = append(yamls, pathlib.Glob(rootdir, "*.yml")...) + diagnoseFilesUnmarshal(yaml.Unmarshal, "YAML", rootdir, yamls, target) +} + func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus) { config, err := robot.LoadRobotYaml(robotfile, false) diagnose := target.Diagnose("Robot") @@ -239,6 +281,7 @@ func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus) { } else { config.Diagnostics(target) } + addFileDiagnostics(filepath.Dir(robotfile), target) } func RunRobotDiagnostics(robotfile string) *common.DiagnosticStatus { diff --git a/pathlib/walk.go b/pathlib/walk.go index 9bedd98a..b9f6a13b 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -150,3 +150,16 @@ func Walk(directory string, ignore Ignore, report Report) error { } return recursiveWalk(fullpath, ".", ignore, report) } + +func Glob(directory string, pattern string) []string { + result := []string{} + ignore := func(entry os.FileInfo) bool { + match, err := filepath.Match(pattern, entry.Name()) + return err != nil || !entry.IsDir() && !match + } + capture := func(_, localpath string, _ os.FileInfo) { + result = append(result, localpath) + } + Walk(directory, ignore, capture) + return result +} From b5016a18b828107c3f0c13af11ae7e349a7747b0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Feb 2021 16:11:53 +0200 Subject: [PATCH 073/516] RCC-153: event timeline (v9.4.4) - fix: added panic protection to telemetry sending, this closes #13 - added initial support for execution timeline tracking --- cloud/metrics.go | 5 ++++ cmd/rcc/main.go | 2 ++ cmd/root.go | 1 + common/timeline.go | 56 +++++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- conda/workflows.go | 20 ++++++++++++++++ docs/changelog.md | 5 ++++ operations/running.go | 2 ++ 8 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 common/timeline.go diff --git a/cloud/metrics.go b/cloud/metrics.go index ffcbc1eb..7749d9d3 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -20,7 +20,12 @@ const ( ) func sendMetric(kind, name, value string) { + common.Timeline("%s:%s = %s", kind, name, value) defer func() { + status := recover() + if status != nil { + common.Debug("Telemetry panic recovered: %v", status) + } telemetryBarrier.Done() }() client, err := NewClient(metricsHost) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 5264852e..6db29086 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -61,6 +61,8 @@ func markTempForRecycling() { } func main() { + common.Timeline("Start.") + defer common.EndOfTimeline() go startTempRecycling() defer markTempForRecycling() defer os.Stderr.Sync() diff --git a/cmd/root.go b/cmd/root.go index d21fb6e9..973d644e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -87,6 +87,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.DebugFlag, "debug", "", false, "to get debug output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TraceFlag, "trace", "", false, "to get trace output where available (not for production use)") + rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") } func initConfig() { diff --git a/common/timeline.go b/common/timeline.go new file mode 100644 index 00000000..c8dce807 --- /dev/null +++ b/common/timeline.go @@ -0,0 +1,56 @@ +package common + +import ( + "fmt" + "time" +) + +var ( + TimelineEnabled bool + pipe chan string + done chan bool +) + +type timevent struct { + when int64 + what string +} + +func timeliner(events chan string, done chan bool) { + birth := time.Now() + history := make([]*timevent, 0, 100) + for { + event, ok := <-events + if !ok { + break + } + history = append(history, &timevent{time.Since(birth).Milliseconds(), event}) + } + death := time.Since(birth).Milliseconds() + if TimelineEnabled && death > 0 { + history = append(history, &timevent{death, "Now."}) + Log("---- rcc timeline ----") + Log(" # 1/1000 millis event") + for at, event := range history { + permille := event.when * 1000 / death + Log("%2d: %4d‰ %6d %s", at+1, permille, event.when, event.what) + } + Log("---- rcc timeline ----") + } + close(done) +} + +func init() { + pipe = make(chan string) + done = make(chan bool) + go timeliner(pipe, done) +} + +func Timeline(form string, details ...interface{}) { + pipe <- fmt.Sprintf(form, details...) +} + +func EndOfTimeline() { + close(pipe) + <-done +} diff --git a/common/version.go b/common/version.go index b909dcc3..f5f37071 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.4.3` + Version = `v9.4.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index b7ea7181..d6025fd0 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -124,14 +124,17 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, error) { targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") + common.Timeline("pre cleanup phase.") err := removeClone(targetFolder) if err != nil { return false, err } common.Debug("=== new live --- first try phase ===") + common.Timeline("first try.") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { common.Debug("=== new live --- second try phase ===") + common.Timeline("second try.") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") err = removeClone(targetFolder) @@ -156,11 +159,14 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") + common.Timeline("Micromamba start.") code, err := shell.New(CondaEnvironment(), ".", command...).StderrOnly().Observed(observer, false) if err != nil || code != 0 { + common.Timeline("micromamba fail.") common.Fatal("Micromamba", err) return false, false } + common.Timeline("micromamba done.") if observer.HasFailures(targetFolder) { return false, true } @@ -168,8 +174,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { common.Log("#### Progress: 4/6 [pip install phase skipped -- no pip dependencies]") + common.Timeline("4/6 no pip.") } else { common.Log("#### Progress: 4/6 [pip install phase]") + common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet"} if common.DebugFlag { @@ -178,11 +186,14 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== new live --- pip install phase ===") err = LiveExecution(targetFolder, pipCommand...) if err != nil { + common.Timeline("pip fail.") common.Fatal("Pip", err) return false, false } + common.Timeline("pip done.") } if postInstall != nil && len(postInstall) > 0 { + common.Timeline("post install.") common.Debug("=== new live --- post install phase ===") for _, script := range postInstall { scriptCommand, err := shlex.Split(script) @@ -268,6 +279,7 @@ func CalculateComboHash(configurations ...string) (string, error) { } func NewEnvironment(force bool, configurations ...string) (string, error) { + common.Timeline("New environment.") cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := RobocorpLock() @@ -291,8 +303,10 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { defer func() { templates = len(TemplateList()) common.Log("#### Progress: 6/6 [Done.] [Cache statistics: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) + common.Timeline("6/6 Done.") }() common.Log("#### Progress: 0/6 [try use existing live same environment?] %v", xviper.TrackingIdentity()) + common.Timeline("0/6 Existing.") xviper.Set("stats.env.request", requests) @@ -314,6 +328,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { defer os.Remove(condaYaml) defer os.Remove(requirementsText) common.Log("#### Progress: 1/6 [environment key is: %s]", key) + common.Timeline("1/6 key %s.", key) common.EnvironmentHash = key @@ -337,8 +352,10 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } if common.Stageonly { common.Log("#### Progress: 2/6 [skipped -- stage only]") + common.Timeline("2/6 stage only.") } else { common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) + common.Timeline("2/6 base to live.") success, err := CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) if err != nil { return "", err @@ -350,6 +367,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } } common.Log("#### Progress: 3/6 [try create new environment from scratch]") + common.Timeline("3/6 env from scratch.") success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) if err != nil { return "", err @@ -359,8 +377,10 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { xviper.Set("stats.env.miss", misses) if common.Liveonly { common.Log("#### Progress: 5/6 [skipped -- live only]") + common.Timeline("5/6 live only.") } else { common.Log("#### Progress: 5/6 [backup new environment as template]") + common.Timeline("5/6 backup to base.") _, err = CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) if err != nil { return "", err diff --git a/docs/changelog.md b/docs/changelog.md index cb6b5583..74d9b126 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.4.4 (date: 24.2.2021) + +- fix: added panic protection to telemetry sending, this closes #13 +- added initial support for execution timeline tracking + ## v9.4.3 (date: 23.2.2021) - added generic reading and parsing diagnostics for JSON and YAML files diff --git a/operations/running.go b/operations/running.go index dfdfaab4..f567a9b5 100644 --- a/operations/running.go +++ b/operations/running.go @@ -73,6 +73,8 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. } func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { + common.Timeline("execution starts.") + defer common.Timeline("execution done.") if simple { ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { From 8f05d1035b5b4e881892ab8de3ba545c0872f42f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 25 Feb 2021 12:15:15 +0200 Subject: [PATCH 074/516] RCC-155: environment modification detection (v9.5.0) - added support for detecting environment corruption - now dirhash command can be used to compare environment content --- cmd/dirhash.go | 41 ++++++++++++++++- common/version.go | 2 +- conda/robocorp.go | 8 +++- conda/workflows.go | 6 +-- docs/changelog.md | 5 +++ operations/running.go | 102 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 7 deletions(-) diff --git a/cmd/dirhash.go b/cmd/dirhash.go index acfadecc..6409380f 100644 --- a/cmd/dirhash.go +++ b/cmd/dirhash.go @@ -1,14 +1,24 @@ package cmd import ( + "fmt" "os" + "path/filepath" + "sort" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) +var ( + showDiff bool + showIntermediateDirhashes bool +) + var dirhashCmd = &cobra.Command{ Use: "dirhash", Short: "Calculate hash for directory content.", @@ -16,6 +26,7 @@ var dirhashCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { defer common.Stopwatch("rcc dirhash lasted").Report() + diffMaps := make([]map[string]string, 0, len(args)) for _, directory := range args { stat, err := os.Stat(directory) if err != nil { @@ -25,17 +36,45 @@ var dirhashCmd = &cobra.Command{ if !stat.IsDir() { continue } - digest, err := conda.DigestFor(directory) + fullpath, err := filepath.Abs(directory) + if err != nil { + continue + } + collector := make(map[string]string) + digest, err := conda.DigestFor(fullpath, collector) if err != nil { common.Error("dirhash", err) continue } + collector = operations.MakeRelativeMap(fullpath, collector) + diffMaps = append(diffMaps, collector) result := conda.Hexdigest(digest) common.Log("+ %v %v", result, directory) + if showIntermediateDirhashes { + relative := make(map[string]string) + keyset := make([]string, 0, len(collector)) + for key, value := range collector { + keyset = append(keyset, key) + relative[key] = value + } + sort.Strings(keyset) + for _, key := range keyset { + fmt.Printf("%s %s\n", relative[key], key) + } + fmt.Println() + } + } + if showDiff && len(diffMaps) != 2 { + pretty.Exit(1, "Diff expects exactly 2 environments, now got %d!", len(diffMaps)) + } + if showDiff { + operations.DirhashDiff(diffMaps[0], diffMaps[1], false) } }, } func init() { internalCmd.AddCommand(dirhashCmd) + dirhashCmd.Flags().BoolVarP(&showIntermediateDirhashes, "print", "", false, "Print all intermediate folder hashes also.") + dirhashCmd.Flags().BoolVarP(&showDiff, "diff", "", false, "Diff two environments with differences.") } diff --git a/common/version.go b/common/version.go index f5f37071..7e8dbf3f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.4.4` + Version = `v9.5.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 8a865f99..48a0c0fc 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -44,7 +44,7 @@ func ignoreDynamicDirectories(folder, entryName string) bool { return name == "__pycache__" || (name == "gen" && base == "comtypes") } -func DigestFor(folder string) ([]byte, error) { +func DigestFor(folder string, collect map[string]string) ([]byte, error) { handle, err := os.Open(folder) if err != nil { return nil, err @@ -61,7 +61,7 @@ func DigestFor(folder string) ([]byte, error) { if ignoreDynamicDirectories(folder, entry.Name()) { continue } - digest, err := DigestFor(filepath.Join(folder, entry.Name())) + digest, err := DigestFor(filepath.Join(folder, entry.Name()), collect) if err != nil { return nil, err } @@ -72,6 +72,10 @@ func DigestFor(folder string) ([]byte, error) { digester.Write([]byte(repr)) } result := digester.Sum([]byte{}) + if collect != nil { + key := fmt.Sprintf("%02x", result) + collect[folder] = key + } return result, nil } diff --git a/conda/workflows.go b/conda/workflows.go index d6025fd0..acbf548b 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -52,7 +52,7 @@ func LastUsed(location string) (time.Time, error) { } func IsPristine(folder string) bool { - digest, err := DigestFor(folder) + digest, err := DigestFor(folder, nil) if err != nil { return false } @@ -219,7 +219,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } - digest, err := DigestFor(targetFolder) + digest, err := DigestFor(targetFolder, nil) if err != nil { common.Fatal("Digest", err) return false, false @@ -454,7 +454,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { } return false, nil } - digest, err := DigestFor(target) + digest, err := DigestFor(target, nil) if err != nil || Hexdigest(digest) != expected { err = removeClone(target) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index 74d9b126..f8b0d3eb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.5.0 (date: 25.2.2021) + +- added support for detecting environment corruption +- now dirhash command can be used to compare environment content + ## v9.4.4 (date: 24.2.2021) - fix: added panic protection to telemetry sending, this closes #13 diff --git a/operations/running.go b/operations/running.go index f567a9b5..13dc93b0 100644 --- a/operations/running.go +++ b/operations/running.go @@ -3,6 +3,7 @@ package operations import ( "fmt" "path/filepath" + "sort" "strings" "github.com/robocorp/rcc/common" @@ -176,14 +177,115 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro environment = append(environment, fmt.Sprintf("%s=%s", key, value)) } } + before := make(map[string]string) + beforeHash, beforeErr := conda.DigestFor(label, before) outputDir := todo.ArtifactDirectory(config) if !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } common.Debug("DEBUG: about to run command - %v", task) _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + after := make(map[string]string) + afterHash, afterErr := conda.DigestFor(label, after) + diagnoseLive(label, beforeHash, afterHash, beforeErr, afterErr, before, after) if err != nil { pretty.Exit(9, "Error: %v", err) } pretty.Ok() } + +func MakeRelativeMap(root string, entries map[string]string) map[string]string { + result := make(map[string]string) + for key, value := range entries { + if !strings.HasPrefix(key, root) { + result[key] = value + continue + } + short, err := filepath.Rel(root, key) + if err == nil { + key = short + } + result[key] = value + } + return result +} + +func DirhashDiff(history, future map[string]string, warning bool) { + removed := []string{} + added := []string{} + changed := []string{} + for key, value := range history { + next, ok := future[key] + if !ok { + removed = append(removed, key) + continue + } + if value != next { + changed = append(changed, key) + } + } + for key, _ := range future { + _, ok := history[key] + if !ok { + added = append(added, key) + } + } + if len(removed)+len(added)+len(changed) == 0 { + return + } + common.Log("---- rcc env diff ----") + sort.Strings(removed) + sort.Strings(added) + sort.Strings(changed) + separate := false + for _, folder := range removed { + common.Log("- diff: removed %q", folder) + separate = true + } + if len(changed) > 0 { + if separate { + common.Log("-------") + separate = false + } + for _, folder := range changed { + common.Log("- diff: changed %q", folder) + separate = true + } + } + if len(added) > 0 { + if separate { + common.Log("-------") + separate = false + } + for _, folder := range added { + common.Log("- diff: added %q", folder) + separate = true + } + } + if warning { + if separate { + common.Log("-------") + separate = false + } + common.Log("Notice: Robot run modified the environment which will slow down the next run.") + common.Log(" Please inform the robot developer about this.") + } + common.Log("---- rcc env diff ----") +} + +func diagnoseLive(label string, beforeHash, afterHash []byte, beforeErr, afterErr error, beforeDetails, afterDetails map[string]string) { + if beforeErr != nil || afterErr != nil { + common.Debug("live %q diagnosis failed, before: %v, after: %v", label, beforeErr, afterErr) + return + } + beforeSummary := fmt.Sprintf("%02x", beforeHash) + afterSummary := fmt.Sprintf("%02x", afterHash) + if beforeSummary == afterSummary { + common.Debug("live %q diagnosis: did not change during run [%s]", label, afterSummary) + return + } + common.Debug("live %q diagnosis: corrupted [%s] => [%s]", label, beforeSummary, afterSummary) + beforeDetails = MakeRelativeMap(label, beforeDetails) + afterDetails = MakeRelativeMap(label, afterDetails) + DirhashDiff(beforeDetails, afterDetails, true) +} From a34edf43405a18d6505e2b88038f26ee65dc6402 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 25 Feb 2021 16:55:24 +0200 Subject: [PATCH 075/516] RCC-155: environment modification detection (v9.5.1) - now also printing environment differences when live is dirty and base is not, just before restoring live from base --- cmd/dirhash.go | 5 +- common/version.go | 2 +- conda/diagnosis.go | 106 ++++++++++++++++++++++++++++++++++++++++++ conda/workflows.go | 10 +++- docs/changelog.md | 5 ++ operations/running.go | 99 +-------------------------------------- 6 files changed, 124 insertions(+), 103 deletions(-) create mode 100644 conda/diagnosis.go diff --git a/cmd/dirhash.go b/cmd/dirhash.go index 6409380f..07b5e009 100644 --- a/cmd/dirhash.go +++ b/cmd/dirhash.go @@ -8,7 +8,6 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -46,7 +45,7 @@ var dirhashCmd = &cobra.Command{ common.Error("dirhash", err) continue } - collector = operations.MakeRelativeMap(fullpath, collector) + collector = conda.MakeRelativeMap(fullpath, collector) diffMaps = append(diffMaps, collector) result := conda.Hexdigest(digest) common.Log("+ %v %v", result, directory) @@ -68,7 +67,7 @@ var dirhashCmd = &cobra.Command{ pretty.Exit(1, "Diff expects exactly 2 environments, now got %d!", len(diffMaps)) } if showDiff { - operations.DirhashDiff(diffMaps[0], diffMaps[1], false) + conda.DirhashDiff(diffMaps[0], diffMaps[1], false) } }, } diff --git a/common/version.go b/common/version.go index 7e8dbf3f..6385d9fa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.5.0` + Version = `v9.5.1` ) diff --git a/conda/diagnosis.go b/conda/diagnosis.go new file mode 100644 index 00000000..09050f85 --- /dev/null +++ b/conda/diagnosis.go @@ -0,0 +1,106 @@ +package conda + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/robocorp/rcc/common" +) + +func MakeRelativeMap(root string, entries map[string]string) map[string]string { + result := make(map[string]string) + for key, value := range entries { + if !strings.HasPrefix(key, root) { + result[key] = value + continue + } + short, err := filepath.Rel(root, key) + if err == nil { + key = short + } + result[key] = value + } + return result +} + +func DirhashDiff(history, future map[string]string, warning bool) { + removed := []string{} + added := []string{} + changed := []string{} + for key, value := range history { + next, ok := future[key] + if !ok { + removed = append(removed, key) + continue + } + if value != next { + changed = append(changed, key) + } + } + for key, _ := range future { + _, ok := history[key] + if !ok { + added = append(added, key) + } + } + if len(removed)+len(added)+len(changed) == 0 { + return + } + common.Log("---- rcc env diff ----") + sort.Strings(removed) + sort.Strings(added) + sort.Strings(changed) + separate := false + for _, folder := range removed { + common.Log("- diff: removed %q", folder) + separate = true + } + if len(changed) > 0 { + if separate { + common.Log("-------") + separate = false + } + for _, folder := range changed { + common.Log("- diff: changed %q", folder) + separate = true + } + } + if len(added) > 0 { + if separate { + common.Log("-------") + separate = false + } + for _, folder := range added { + common.Log("- diff: added %q", folder) + separate = true + } + } + if warning { + if separate { + common.Log("-------") + separate = false + } + common.Log("Notice: Robot run modified the environment which will slow down the next run.") + common.Log(" Please inform the robot developer about this.") + } + common.Log("---- rcc env diff ----") +} + +func DiagnoseDirty(beforeLabel, afterLabel string, beforeHash, afterHash []byte, beforeErr, afterErr error, beforeDetails, afterDetails map[string]string) { + if beforeErr != nil || afterErr != nil { + common.Debug("live %q diagnosis failed, before: %v, after: %v", afterLabel, beforeErr, afterErr) + return + } + beforeSummary := fmt.Sprintf("%02x", beforeHash) + afterSummary := fmt.Sprintf("%02x", afterHash) + if beforeSummary == afterSummary { + common.Debug("live %q diagnosis: did not change during run [%s]", afterLabel, afterSummary) + return + } + common.Debug("live %q diagnosis: corrupted [%s] => [%s]", afterLabel, beforeSummary, afterSummary) + beforeDetails = MakeRelativeMap(beforeLabel, beforeDetails) + afterDetails = MakeRelativeMap(afterLabel, afterDetails) + DirhashDiff(beforeDetails, afterDetails, true) +} diff --git a/conda/workflows.go b/conda/workflows.go index acbf548b..c19af8de 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -341,6 +341,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } liveFolder := LiveFrom(key) + after := make(map[string]string) + afterHash, afterErr := DigestFor(liveFolder, after) reusable, err := reuseExistingLive(key) if err != nil { return "", err @@ -354,9 +356,15 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { common.Log("#### Progress: 2/6 [skipped -- stage only]") common.Timeline("2/6 stage only.") } else { + templateFolder := TemplateFrom(key) + if IsPristine(templateFolder) { + before := make(map[string]string) + beforeHash, beforeErr := DigestFor(templateFolder, before) + DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after) + } common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) common.Timeline("2/6 base to live.") - success, err := CloneFromTo(TemplateFrom(key), liveFolder, pathlib.CopyFile) + success, err := CloneFromTo(templateFolder, liveFolder, pathlib.CopyFile) if err != nil { return "", err } diff --git a/docs/changelog.md b/docs/changelog.md index f8b0d3eb..360e5113 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.5.1 (date: 25.2.2021) + +- now also printing environment differences when live is dirty and base + is not, just before restoring live from base + ## v9.5.0 (date: 25.2.2021) - added support for detecting environment corruption diff --git a/operations/running.go b/operations/running.go index 13dc93b0..d913c74d 100644 --- a/operations/running.go +++ b/operations/running.go @@ -3,7 +3,6 @@ package operations import ( "fmt" "path/filepath" - "sort" "strings" "github.com/robocorp/rcc/common" @@ -187,105 +186,9 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) - diagnoseLive(label, beforeHash, afterHash, beforeErr, afterErr, before, after) + conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after) if err != nil { pretty.Exit(9, "Error: %v", err) } pretty.Ok() } - -func MakeRelativeMap(root string, entries map[string]string) map[string]string { - result := make(map[string]string) - for key, value := range entries { - if !strings.HasPrefix(key, root) { - result[key] = value - continue - } - short, err := filepath.Rel(root, key) - if err == nil { - key = short - } - result[key] = value - } - return result -} - -func DirhashDiff(history, future map[string]string, warning bool) { - removed := []string{} - added := []string{} - changed := []string{} - for key, value := range history { - next, ok := future[key] - if !ok { - removed = append(removed, key) - continue - } - if value != next { - changed = append(changed, key) - } - } - for key, _ := range future { - _, ok := history[key] - if !ok { - added = append(added, key) - } - } - if len(removed)+len(added)+len(changed) == 0 { - return - } - common.Log("---- rcc env diff ----") - sort.Strings(removed) - sort.Strings(added) - sort.Strings(changed) - separate := false - for _, folder := range removed { - common.Log("- diff: removed %q", folder) - separate = true - } - if len(changed) > 0 { - if separate { - common.Log("-------") - separate = false - } - for _, folder := range changed { - common.Log("- diff: changed %q", folder) - separate = true - } - } - if len(added) > 0 { - if separate { - common.Log("-------") - separate = false - } - for _, folder := range added { - common.Log("- diff: added %q", folder) - separate = true - } - } - if warning { - if separate { - common.Log("-------") - separate = false - } - common.Log("Notice: Robot run modified the environment which will slow down the next run.") - common.Log(" Please inform the robot developer about this.") - } - common.Log("---- rcc env diff ----") -} - -func diagnoseLive(label string, beforeHash, afterHash []byte, beforeErr, afterErr error, beforeDetails, afterDetails map[string]string) { - if beforeErr != nil || afterErr != nil { - common.Debug("live %q diagnosis failed, before: %v, after: %v", label, beforeErr, afterErr) - return - } - beforeSummary := fmt.Sprintf("%02x", beforeHash) - afterSummary := fmt.Sprintf("%02x", afterHash) - if beforeSummary == afterSummary { - common.Debug("live %q diagnosis: did not change during run [%s]", label, afterSummary) - return - } - common.Debug("live %q diagnosis: corrupted [%s] => [%s]", label, beforeSummary, afterSummary) - beforeDetails = MakeRelativeMap(label, beforeDetails) - afterDetails = MakeRelativeMap(label, afterDetails) - DirhashDiff(beforeDetails, afterDetails, true) -} From 07b2ca67ee84cd3e555cc2d5684e5350977d41c9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 26 Feb 2021 13:15:01 +0200 Subject: [PATCH 076/516] RCC-156: bug --liveonly breaks when broken base (v9.5.2) - bug fix: now cloning sources are not removed during --liveonly action, even when that source seems to be invalid - changed timeline to use percent (not permilles anymore) - minor fix on env diff printout --- common/timeline.go | 5 +++-- common/version.go | 2 +- conda/diagnosis.go | 4 ++-- conda/workflows.go | 12 ++++++++---- docs/changelog.md | 7 +++++++ operations/running.go | 2 +- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/common/timeline.go b/common/timeline.go index c8dce807..ad69a05d 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -30,10 +30,11 @@ func timeliner(events chan string, done chan bool) { if TimelineEnabled && death > 0 { history = append(history, &timevent{death, "Now."}) Log("---- rcc timeline ----") - Log(" # 1/1000 millis event") + Log(" # percent millis event") for at, event := range history { permille := event.when * 1000 / death - Log("%2d: %4d‰ %6d %s", at+1, permille, event.when, event.what) + percent := float64(permille) / 10.0 + Log("%2d: %5.1f%% %6d %s", at+1, percent, event.when, event.what) } Log("---- rcc timeline ----") } diff --git a/common/version.go b/common/version.go index 6385d9fa..bcb01c56 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.5.1` + Version = `v9.5.2` ) diff --git a/conda/diagnosis.go b/conda/diagnosis.go index 09050f85..2be7b0b3 100644 --- a/conda/diagnosis.go +++ b/conda/diagnosis.go @@ -88,7 +88,7 @@ func DirhashDiff(history, future map[string]string, warning bool) { common.Log("---- rcc env diff ----") } -func DiagnoseDirty(beforeLabel, afterLabel string, beforeHash, afterHash []byte, beforeErr, afterErr error, beforeDetails, afterDetails map[string]string) { +func DiagnoseDirty(beforeLabel, afterLabel string, beforeHash, afterHash []byte, beforeErr, afterErr error, beforeDetails, afterDetails map[string]string, warning bool) { if beforeErr != nil || afterErr != nil { common.Debug("live %q diagnosis failed, before: %v, after: %v", afterLabel, beforeErr, afterErr) return @@ -102,5 +102,5 @@ func DiagnoseDirty(beforeLabel, afterLabel string, beforeHash, afterHash []byte, common.Debug("live %q diagnosis: corrupted [%s] => [%s]", afterLabel, beforeSummary, afterSummary) beforeDetails = MakeRelativeMap(beforeLabel, beforeDetails) afterDetails = MakeRelativeMap(afterLabel, afterDetails) - DirhashDiff(beforeDetails, afterDetails, true) + DirhashDiff(beforeDetails, afterDetails, warning) } diff --git a/conda/workflows.go b/conda/workflows.go index c19af8de..45b6548e 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -360,7 +360,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { if IsPristine(templateFolder) { before := make(map[string]string) beforeHash, beforeErr := DigestFor(templateFolder, before) - DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after) + DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after, false) } common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) common.Timeline("2/6 base to live.") @@ -444,9 +444,13 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { os.MkdirAll(target, 0755) if !IsPristine(source) { - err = removeClone(source) - if err != nil { - return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) + if common.Liveonly { + common.Debug("Clone source %q is dirty, but wont remove since --liveonly flag.", source) + } else { + err = removeClone(source) + if err != nil { + return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) + } } return false, nil } diff --git a/docs/changelog.md b/docs/changelog.md index 360e5113..cb407834 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.5.2 (date: 25.2.2021) + +- bug fix: now cloning sources are not removed during --liveonly action, + even when that source seems to be invalid +- changed timeline to use percent (not permilles anymore) +- minor fix on env diff printout + ## v9.5.1 (date: 25.2.2021) - now also printing environment differences when live is dirty and base diff --git a/operations/running.go b/operations/running.go index d913c74d..5c1dad11 100644 --- a/operations/running.go +++ b/operations/running.go @@ -186,7 +186,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) - conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after) + conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after, true) if err != nil { pretty.Exit(9, "Error: %v", err) } From 483f54722a9819a6a460393072fdf6f92330ed57 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 2 Mar 2021 09:45:32 +0200 Subject: [PATCH 077/516] FEATURE: interactive run enabled (v9.5.3) - added `--interactive` flag to `rcc task run` command, so that developers can use debuggers and other interactive tools while debugging --- cmd/run.go | 8 +++++--- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 3f9fe59e..4528995a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -12,8 +12,9 @@ import ( ) var ( - rcHosts = []string{"RC_API_SECRET_HOST", "RC_API_WORKITEM_HOST"} - rcTokens = []string{"RC_API_SECRET_TOKEN", "RC_API_WORKITEM_TOKEN"} + rcHosts = []string{"RC_API_SECRET_HOST", "RC_API_WORKITEM_HOST"} + rcTokens = []string{"RC_API_SECRET_TOKEN", "RC_API_WORKITEM_TOKEN"} + interactiveFlag bool ) var runCmd = &cobra.Command{ @@ -34,7 +35,7 @@ in your own machine.`, defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) - operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) + operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, interactiveFlag, nil) }, } @@ -59,4 +60,5 @@ func init() { runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") + runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in teminal/command prompt. For development only, not for production!") } diff --git a/common/version.go b/common/version.go index bcb01c56..040b3520 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.5.2` + Version = `v9.5.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index cb407834..66dcbad3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.5.3 (date: 2.3.2021) + +- added `--interactive` flag to `rcc task run` command, so that developers + can use debuggers and other interactive tools while debugging + ## v9.5.2 (date: 25.2.2021) - bug fix: now cloning sources are not removed during --liveonly action, From 2fd66434f0a3607d168d732a46bda2718fbe1e4a Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Tue, 2 Mar 2021 11:06:11 +0200 Subject: [PATCH 078/516] Updated rpaframework in templates (#14) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ templates/extended/conda.yaml | 2 +- templates/python/conda.yaml | 2 +- templates/standard/conda.yaml | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index 040b3520..eb589ccc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.5.3` + Version = `v9.5.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 66dcbad3..23a33a58 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.5.4 (date: 2.3.2021) + +- Updated rpaframework to version 7.6.0 in templates + ## v9.5.3 (date: 2.3.2021) - added `--interactive` flag to `rcc task run` command, so that developers diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index 56b4a75b..e6db2250 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html + - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index 56b4a75b..e6db2250 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html + - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index 56b4a75b..e6db2250 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.1.1 # https://rpaframework.org/releasenotes.html + - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html From 51e213abc564f18e0c09131fc5f1303b0064ea8c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 3 Mar 2021 08:00:59 +0200 Subject: [PATCH 079/516] RCC-155: assistant install (v9.6.0) - new command `rcc cloud prepare` to support installing assistants on local computer for faster startup time - added more timeline entries on relevant parts --- cmd/assistantRun.go | 1 + cmd/cloudPrepare.go | 71 ++++++++++++++++++++++++++++++++++++++++ common/variables.go | 6 ++++ common/version.go | 2 +- conda/download.go | 1 + docs/changelog.md | 6 ++++ operations/assistant.go | 3 ++ operations/authorize.go | 2 ++ operations/robotcache.go | 1 + operations/updownload.go | 1 + operations/zipper.go | 1 + pretty/functions.go | 7 ++++ 12 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 cmd/cloudPrepare.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 8726f845..3a3afacc 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -93,6 +93,7 @@ var assistantRunCmd = &cobra.Command{ cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.String()) }() defer func() { + common.Timeline("publish artifacts") publisher := operations.ArtifactPublisher{ Client: client, ArtifactPostURL: assistant.ArtifactURL, diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go new file mode 100644 index 00000000..de993786 --- /dev/null +++ b/cmd/cloudPrepare.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" + + "github.com/spf13/cobra" +) + +var prepareCloudCmd = &cobra.Command{ + Use: "prepare", + Short: "Prepare cloud robot for fast startup time in local computer.", + Long: "Prepare cloud robot for fast startup time in local computer.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Cloud prepare lasted").Report() + } + + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) + defer os.Remove(zipfile) + + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) + defer os.RemoveAll(workarea) + + pretty.Guard(conda.MustMicromamba(), 1, "Could not get micromamba installed.") + + account := operations.AccountByName(AccountName()) + pretty.Guard(account != nil, 2, "Could not find account by name: %q", AccountName()) + + client, err := cloud.NewClient(account.Endpoint) + pretty.Guard(err == nil, 3, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) + + err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + pretty.Guard(err == nil, 4, "Error: %v", err) + + common.Debug("Using temporary workarea: %v", workarea) + err = operations.Unzip(workarea, zipfile, false, true) + pretty.Guard(err == nil, 5, "Error: %v", err) + + robotfile, err := pathlib.FindNamedPath(workarea, "robot.yaml") + pretty.Guard(err == nil, 6, "Error: %v", err) + + config, err := robot.LoadRobotYaml(robotfile, false) + pretty.Guard(err == nil, 7, "Error: %v", err) + pretty.Guard(config.UsesConda(), 0, "Ok.") + + condafile := config.CondaConfigFile() + label, err := conda.NewEnvironment(false, condafile) + pretty.Guard(err == nil, 8, "Error: %v", err) + + common.Log("Prepared %q.", label) + pretty.Ok() + }, +} + +func init() { + cloudCmd.AddCommand(prepareCloudCmd) + prepareCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("workspace") + prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("robot") +} diff --git a/common/variables.go b/common/variables.go index 580027de..6cff8156 100644 --- a/common/variables.go +++ b/common/variables.go @@ -3,6 +3,7 @@ package common import ( "fmt" "strings" + "time" ) var ( @@ -17,12 +18,17 @@ var ( ControllerType string LeaseContract string EnvironmentHash string + When int64 ) const ( DefaultEndpoint = "https://api.eu1.robocloud.eu/" ) +func init() { + When = time.Now().Unix() +} + func UnifyVerbosityFlags() { if Silent { DebugFlag = false diff --git a/common/version.go b/common/version.go index eb589ccc..a920badd 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.5.4` + Version = `v9.6.0` ) diff --git a/conda/download.go b/conda/download.go index 5caa263d..50179945 100644 --- a/conda/download.go +++ b/conda/download.go @@ -13,6 +13,7 @@ import ( ) func DownloadMicromamba() error { + common.Timeline("downloading micromamba") url := MicromambaLink() filename := BinMicromamba() response, err := http.Get(url) diff --git a/docs/changelog.md b/docs/changelog.md index 23a33a58..b373c133 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.6.0 (date: 3.3.2021) + +- new command `rcc cloud prepare` to support installing assistants on + local computer for faster startup time +- added more timeline entries on relevant parts + ## v9.5.4 (date: 2.3.2021) - Updated rpaframework to version 7.6.0 in templates diff --git a/operations/assistant.go b/operations/assistant.go index 016289f9..0cb16239 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -254,6 +254,7 @@ func BackgroundAssistantHeartbeat(cancel chan bool, client cloud.Client, account } func BeatAssistantRun(client cloud.Client, account *account, workspaceId, assistantId, runId string, beat int) error { + common.Timeline("send assistant heartbeat") credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { return err @@ -274,6 +275,7 @@ func BeatAssistantRun(client cloud.Client, account *account, workspaceId, assist } func StopAssistantRun(client cloud.Client, account *account, workspaceId, assistantId, runId, status, reason string) error { + common.Timeline("stop assistant run: %s", assistantId) credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { return err @@ -295,6 +297,7 @@ func StopAssistantRun(client cloud.Client, account *account, workspaceId, assist } func StartAssistantRun(client cloud.Client, account *account, workspaceId, assistantId string) (*AssistantRobot, error) { + common.Timeline("start assistant run: %q", assistantId) credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { return nil, err diff --git a/operations/authorize.go b/operations/authorize.go index f037c2a2..f67c7454 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -10,6 +10,7 @@ import ( "time" "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" ) const ( @@ -188,6 +189,7 @@ func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { } func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (Token, error) { + common.Timeline("authorize %s", claims.Name) when := time.Now().Unix() found, ok := account.Cached(claims.Name, claims.Url) if ok { diff --git a/operations/robotcache.go b/operations/robotcache.go index f79ad73a..80c61be8 100644 --- a/operations/robotcache.go +++ b/operations/robotcache.go @@ -17,6 +17,7 @@ var ( ) func CacheRobot(filename string) error { + common.Timeline("caching robot: %s", filename) fullpath, err := filepath.Abs(filename) if err != nil { return err diff --git a/operations/updownload.go b/operations/updownload.go index 7930f5cf..e921bb7b 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -136,6 +136,7 @@ func UploadCommand(client cloud.Client, account *account, workspaceId, robotId, } func DownloadCommand(client cloud.Client, account *account, workspaceId, robotId, zipfile string, debug bool) error { + common.Timeline("download started: %s", zipfile) token, err := summonRobotToken(client, account, workspaceId) if err != nil { return err diff --git a/operations/zipper.go b/operations/zipper.go index 9026d301..74c67db9 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -252,6 +252,7 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { } func Unzip(directory, zipfile string, force, temporary bool) error { + common.Timeline("unzip %q to %q", zipfile, directory) fullpath, err := filepath.Abs(directory) if err != nil { return err diff --git a/pretty/functions.go b/pretty/functions.go index 769c372a..117d3a1d 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -25,3 +25,10 @@ func Exit(code int, format string, rest ...interface{}) { } common.Exit(code, niceform, rest...) } + +// Guard watches, that only truthful shall pass. Otherwise exits with code and details. +func Guard(truth bool, code int, format string, rest ...interface{}) { + if !truth { + Exit(code, format, rest...) + } +} From 662a24e6e16e971433963109b599fbe137155df8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 3 Mar 2021 09:23:27 +0200 Subject: [PATCH 080/516] REFACTOR: common When usage (v9.6.1) - refactored code use common.When as consistent timestamp for current rcc run --- cmd/assistantRun.go | 4 +--- cmd/carrier.go | 3 +-- cmd/communitypull.go | 3 +-- cmd/pull.go | 3 +-- cmd/push.go | 3 +-- cmd/testrun.go | 5 ++--- common/version.go | 2 +- conda/workflows.go | 5 ++--- docs/changelog.md | 4 ++++ operations/authorize.go | 22 +++++++++------------- operations/credentials.go | 2 +- operations/robots.go | 15 +++++++-------- operations/updownload.go | 3 +-- xviper/tracking.go | 4 +++- 14 files changed, 35 insertions(+), 43 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 3a3afacc..ccd83ca4 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -30,8 +30,6 @@ var assistantRunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Robot Assistant run lasted").Report() } - now := time.Now() - marker := now.Unix() ok := conda.MustMicromamba() if !ok { pretty.Exit(2, "Could not get micromamba installed.") @@ -67,7 +65,7 @@ var assistantRunCmd = &cobra.Command{ common.Debug("Robot Assistant run-id is %v.", assistant.RunId) common.Debug("With task '%v' from zip %v.", assistant.TaskName, assistant.Zipfile) sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", marker)) + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) reason = "UNZIP_FAILURE" diff --git a/cmd/carrier.go b/cmd/carrier.go index 05a169e2..a79456b5 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -49,14 +49,13 @@ func runCarrier() error { } defer xviper.RunMinutes().Done() now := time.Now() - marker := now.Unix() testrunDir := filepath.Join(".", now.Format("2006-01-02_15_04_05")) err = os.MkdirAll(testrunDir, 0o755) if err != nil { return err } sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", marker)) + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) carrier, err := operations.FindExecutable() diff --git a/cmd/communitypull.go b/cmd/communitypull.go index 8dfc5dd1..6cbf280b 100644 --- a/cmd/communitypull.go +++ b/cmd/communitypull.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" @@ -27,7 +26,7 @@ var communityPullCmd = &cobra.Command{ defer common.Stopwatch("Pull lasted").Report() } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", time.Now().Unix())) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/pull.go b/cmd/pull.go index eab61860..c9ee4ff3 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -33,7 +32,7 @@ var pullCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v reason %v", account.Endpoint, err) } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", time.Now().Unix())) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/push.go b/cmd/push.go index 8a17d29f..27ddadf6 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -31,7 +30,7 @@ var pushCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v reason: %v", account.Endpoint, err) } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("push%x.zip", time.Now().Unix())) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("push%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/testrun.go b/cmd/testrun.go index 0f229e20..69ad9469 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -33,8 +33,7 @@ var testrunCmd = &cobra.Command{ } defer xviper.RunMinutes().Done() now := time.Now() - marker := now.Unix() - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("testrun%x.zip", marker)) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("testrun%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zip file: %v", zipfile) sourceDir := filepath.Dir(robotFile) @@ -48,7 +47,7 @@ var testrunCmd = &cobra.Command{ pretty.Exit(2, "Error: %v", err) } sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", marker)) + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) err = operations.Unzip(workarea, zipfile, false, true) diff --git a/common/version.go b/common/version.go index a920badd..0e6e0dc3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.6.0` + Version = `v9.6.1` ) diff --git a/conda/workflows.go b/conda/workflows.go index 45b6548e..c94d8b93 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -315,9 +315,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { xviper.Set("stats.env.merges", merges) } - marker := time.Now().Unix() - condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", marker)) - requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", marker)) + condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", common.When)) + requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", common.When)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index b373c133..2d274583 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.6.1 (date: 3.3.2021) + +- refactored code use common.When as consistent timestamp for current rcc run + ## v9.6.0 (date: 3.3.2021) - new command `rcc cloud prepare` to support installing assistants on diff --git a/operations/authorize.go b/operations/authorize.go index f67c7454..401a5d5d 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "strings" - "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -190,14 +189,13 @@ func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (Token, error) { common.Timeline("authorize %s", claims.Name) - when := time.Now().Unix() found, ok := account.Cached(claims.Name, claims.Url) if ok { cached := make(Token) cached["endpoint"] = client.Endpoint() cached["requested"] = claims cached["status"] = "200" - cached["when"] = when + cached["when"] = common.When cached["token"] = found return cached, nil } @@ -207,7 +205,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To } bodyHash := Digest(body) size := len([]byte(body)) - nonce := fmt.Sprintf("%d", when) + nonce := fmt.Sprintf("%d", common.When) signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson @@ -227,11 +225,11 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To token["endpoint"] = client.Endpoint() token["requested"] = claims token["status"] = response.Status - token["when"] = when - account.WasVerified(when) + token["when"] = common.When + account.WasVerified(common.When) trueToken, ok := token["token"].(string) if ok { - deadline := when + int64(3*(claims.ExpiresIn/4)) + deadline := common.When + int64(3*(claims.ExpiresIn/4)) account.CacheToken(claims.Name, claims.Url, trueToken, deadline) } return token, nil @@ -240,8 +238,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To func DeleteAccount(client cloud.Client, account *account) error { claims := DeleteClaims() bodyHash := Digest("{}") - when := time.Now().Unix() - nonce := fmt.Sprintf("%d", when) + nonce := fmt.Sprintf("%d", common.When) signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson @@ -257,8 +254,7 @@ func DeleteAccount(client cloud.Client, account *account) error { func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { claims := VerificationClaims() bodyHash := Digest("{}") - when := time.Now().Unix() - nonce := fmt.Sprintf("%d", when) + nonce := fmt.Sprintf("%d", common.When) signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson @@ -277,9 +273,9 @@ func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { link["endpoint"] = client.Endpoint() link["requested"] = claims link["status"] = response.Status - link["when"] = when + link["when"] = common.When result.Link = link - account.WasVerified(when) + account.WasVerified(common.When) account.UpdateDetails(result.User) return &result, nil } diff --git a/operations/credentials.go b/operations/credentials.go index c8f7ffce..a5286374 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -119,7 +119,7 @@ func (it *account) Cached(name, url string) (string, bool) { if !ok { return "", false } - if found.Deadline < time.Now().Unix() { + if found.Deadline < common.When { return "", false } return found.Token, true diff --git a/operations/robots.go b/operations/robots.go index 10bebc6e..38a51076 100644 --- a/operations/robots.go +++ b/operations/robots.go @@ -4,7 +4,8 @@ import ( "os" "path/filepath" "sort" - "time" + + "github.com/robocorp/rcc/common" ) func UpdateRobot(directory string) error { @@ -17,22 +18,21 @@ func UpdateRobot(directory string) error { return err } defer cache.Save() - now := time.Now().Unix() robot, ok := cache.Robots[fullpath] if !ok { robot = &Folder{ Path: fullpath, - Created: now, - Updated: now, + Created: common.When, + Updated: common.When, Deleted: 0, } cache.Robots[fullpath] = robot } stat, err := os.Stat(fullpath) if err != nil || !stat.IsDir() { - robot.Deleted = now + robot.Deleted = common.When } - robot.Updated = now + robot.Updated = common.When return nil } @@ -50,12 +50,11 @@ func detectDeadRobots() bool { if err != nil { return false } - now := time.Now().Unix() changed := false for _, robot := range cache.Robots { stat, err := os.Stat(robot.Path) if err != nil || !stat.IsDir() { - robot.Deleted = now + robot.Deleted = common.When changed = true continue } diff --git a/operations/updownload.go b/operations/updownload.go index e921bb7b..c39b2147 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -7,7 +7,6 @@ import ( "net/url" "os" "path/filepath" - "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -166,7 +165,7 @@ func SummonRobotZipfile(client cloud.Client, account *account, workspaceId, robo if ok { return found, nil } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("summon%x.zip", time.Now().Unix())) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) err := DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) if err != nil { return "", err diff --git a/xviper/tracking.go b/xviper/tracking.go index a3f4f474..c262c2db 100644 --- a/xviper/tracking.go +++ b/xviper/tracking.go @@ -6,6 +6,8 @@ import ( "math/rand" "strings" "time" + + "github.com/robocorp/rcc/common" ) const ( @@ -18,7 +20,7 @@ var ( ) func init() { - rand.Seed(time.Now().Unix()) + rand.Seed(common.When) } func AsGuid(content []byte) string { From 62880c26350516138c4652de81f65cd612a033a6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 5 Mar 2021 09:19:22 +0200 Subject: [PATCH 081/516] FIX: time format fixes (v9.6.2) --- cloud/client.go | 10 ++++------ cmd/assistantRun.go | 8 ++++---- cmd/root.go | 1 + common/elapsed.go | 28 +++++++++++++++++++++++----- common/elapsed_test.go | 2 +- common/timeline.go | 14 ++++++-------- common/variables.go | 4 +++- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/authorize.go | 2 +- operations/credentials.go | 1 + operations/robotcache.go | 2 +- 12 files changed, 50 insertions(+), 28 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index f1789cc3..106e235e 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "net/http" "strings" - "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/xviper" @@ -30,7 +29,7 @@ type Response struct { Status int Err error Body []byte - Elapsed time.Duration + Elapsed common.Duration } type Client interface { @@ -71,14 +70,13 @@ func (it *internalClient) Endpoint() string { } func (it *internalClient) does(method string, request *Request) *Response { + stopwatch := common.Stopwatch("stopwatch") response := new(Response) - started := time.Now() url := it.Endpoint() + request.Url common.Trace("Doing %s %s", method, url) defer func() { - elapsed := time.Now().Sub(started) - response.Elapsed = elapsed - common.Trace("%s %s took %v", method, url, elapsed) + response.Elapsed = stopwatch.Elapsed() + common.Trace("%s %s took %s", method, url, response.Elapsed) }() httpRequest, err := http.NewRequest(method, url, request.Body) if err != nil { diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index ccd83ca4..4f341179 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -44,7 +44,7 @@ var assistantRunCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } reason = "START_FAILURE" - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.Elapsed().String()) defer func() { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) }() @@ -88,7 +88,7 @@ var assistantRunCmd = &cobra.Command{ } defer func() { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.Elapsed().String()) }() defer func() { common.Timeline("publish artifacts") @@ -105,9 +105,9 @@ var assistantRunCmd = &cobra.Command{ } }() - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.setup", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.setup", elapser.Elapsed().String()) defer func() { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.String()) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.Elapsed().String()) }() reason = "ROBOT_FAILURE" operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) diff --git a/cmd/root.go b/cmd/root.go index 973d644e..b1a8df19 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -101,6 +101,7 @@ func initConfig() { common.UnifyStageHandling() pretty.Setup() + common.Timeline("%q", os.Args) common.Trace("CLI command was: %#v", os.Args) common.Debug("Using config file: %v", xviper.ConfigFileUsed()) conda.ValidateLocations() diff --git a/common/elapsed.go b/common/elapsed.go index 0e260a5c..a14f3310 100644 --- a/common/elapsed.go +++ b/common/elapsed.go @@ -10,6 +10,20 @@ type stopwatch struct { started time.Time } +type Duration time.Duration + +func (it Duration) Truncate(granularity time.Duration) Duration { + return Duration(time.Duration(it).Truncate(granularity)) +} + +func (it Duration) Milliseconds() int64 { + return time.Duration(it).Milliseconds() +} + +func (it Duration) String() string { + return fmt.Sprintf("%5.3f", float64(it.Milliseconds())/1000.0) +} + func Stopwatch(form string, details ...interface{}) *stopwatch { message := fmt.Sprintf(form, details...) return &stopwatch{ @@ -19,18 +33,22 @@ func Stopwatch(form string, details ...interface{}) *stopwatch { } func (it *stopwatch) String() string { - elapsed := time.Now().Sub(it.started) + elapsed := it.Elapsed().Truncate(time.Millisecond) return fmt.Sprintf("%v", elapsed) } -func (it *stopwatch) Log() time.Duration { - elapsed := time.Now().Sub(it.started) +func (it *stopwatch) Elapsed() Duration { + return Duration(time.Since(it.started)) +} + +func (it *stopwatch) Log() Duration { + elapsed := it.Elapsed() Log("%v %v", it.message, elapsed) return elapsed } -func (it *stopwatch) Report() time.Duration { - elapsed := time.Now().Sub(it.started) +func (it *stopwatch) Report() Duration { + elapsed := it.Elapsed() Log("%v %v", it.message, elapsed) return elapsed } diff --git a/common/elapsed_test.go b/common/elapsed_test.go index 0b90f96c..cdc8d7eb 100644 --- a/common/elapsed_test.go +++ b/common/elapsed_test.go @@ -13,6 +13,6 @@ func TestCanUseStopwatch(t *testing.T) { sut := common.Stopwatch("hello") wont_be.Nil(sut) - limit := time.Duration(10) * time.Millisecond + limit := common.Duration(10 * time.Millisecond) must_be.True(sut.Report() < limit) } diff --git a/common/timeline.go b/common/timeline.go index ad69a05d..cd8767bc 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -2,7 +2,6 @@ package common import ( "fmt" - "time" ) var ( @@ -12,29 +11,28 @@ var ( ) type timevent struct { - when int64 + when Duration what string } func timeliner(events chan string, done chan bool) { - birth := time.Now() history := make([]*timevent, 0, 100) for { event, ok := <-events if !ok { break } - history = append(history, &timevent{time.Since(birth).Milliseconds(), event}) + history = append(history, &timevent{Clock.Elapsed(), event}) } - death := time.Since(birth).Milliseconds() - if TimelineEnabled && death > 0 { + death := Clock.Elapsed() + if TimelineEnabled && death.Milliseconds() > 0 { history = append(history, &timevent{death, "Now."}) Log("---- rcc timeline ----") - Log(" # percent millis event") + Log(" # percent seconds event") for at, event := range history { permille := event.when * 1000 / death percent := float64(permille) / 10.0 - Log("%2d: %5.1f%% %6d %s", at+1, percent, event.when, event.what) + Log("%2d: %5.1f%% %7s %s", at+1, percent, event.when, event.what) } Log("---- rcc timeline ----") } diff --git a/common/variables.go b/common/variables.go index 6cff8156..122290d4 100644 --- a/common/variables.go +++ b/common/variables.go @@ -19,6 +19,7 @@ var ( LeaseContract string EnvironmentHash string When int64 + Clock *stopwatch ) const ( @@ -26,7 +27,8 @@ const ( ) func init() { - When = time.Now().Unix() + Clock = &stopwatch{"Clock", time.Now()} + When = Clock.started.Unix() } func UnifyVerbosityFlags() { diff --git a/common/version.go b/common/version.go index 0e6e0dc3..3ca9a910 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.6.1` + Version = `v9.6.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2d274583..abd184a6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.6.2 (date: 5.3.2021) + +- fix for time formats used in timeline, some metrics, and stopwatch + ## v9.6.1 (date: 3.3.2021) - refactored code use common.When as consistent timestamp for current rcc run diff --git a/operations/authorize.go b/operations/authorize.go index 401a5d5d..b6e6c353 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -188,7 +188,6 @@ func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { } func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (Token, error) { - common.Timeline("authorize %s", claims.Name) found, ok := account.Cached(claims.Name, claims.Url) if ok { cached := make(Token) @@ -199,6 +198,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To cached["token"] = found return cached, nil } + common.Timeline("authorize claim: %s (request)", claims.Name) body, err := claims.AsJson() if err != nil { return nil, err diff --git a/operations/credentials.go b/operations/credentials.go index a5286374..db721ae4 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -122,6 +122,7 @@ func (it *account) Cached(name, url string) (string, bool) { if found.Deadline < common.When { return "", false } + common.Timeline("cached token: %s", name) return found.Token, true } diff --git a/operations/robotcache.go b/operations/robotcache.go index 80c61be8..13d00d41 100644 --- a/operations/robotcache.go +++ b/operations/robotcache.go @@ -17,7 +17,6 @@ var ( ) func CacheRobot(filename string) error { - common.Timeline("caching robot: %s", filename) fullpath, err := filepath.Abs(filename) if err != nil { return err @@ -26,6 +25,7 @@ func CacheRobot(filename string) error { if err != nil { return err } + common.Timeline("caching robot: %s -> %s", filename, digest) common.Debug("Digest for %v is %v.", fullpath, digest) _, exists := LookupRobot(digest) if exists { From 1e6d6a36943ec6b9f0f0b15bb579bf6ca17b8dc7 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 10 Mar 2021 11:53:47 +0200 Subject: [PATCH 082/516] RCC-151: activate conda environment (v9.7.0) - conda environments are now activated once on creation, and variables go with environment, as `rcc_activate.json` - there is also now new "installation plan" file inside environment, called `rcc_plan.log` which contains events that lead to activation - normal runs are now more silent, since details are moved into "plan" file --- cmd/internalEnv.go | 38 +++++++ common/version.go | 2 +- conda/activate.go | 172 ++++++++++++++++++++++++++++++++ conda/platform_darwin_amd64.go | 7 ++ conda/platform_linux_amd64.go | 7 ++ conda/platform_windows_amd64.go | 5 + conda/robocorp.go | 2 +- conda/workflows.go | 70 +++++++++++-- docs/changelog.md | 8 ++ operations/running.go | 1 + robot/robot.go | 2 +- shell/task.go | 8 ++ 12 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 cmd/internalEnv.go create mode 100644 conda/activate.go diff --git a/cmd/internalEnv.go b/cmd/internalEnv.go new file mode 100644 index 00000000..f3f2974e --- /dev/null +++ b/cmd/internalEnv.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var preformatLabel string + +var internalEnvCmd = &cobra.Command{ + Use: "env", + Short: "JSON dump of current execution environment.", + Long: "JSON dump of current execution environment.", + Run: func(cmd *cobra.Command, args []string) { + values := make(map[string]string) + for _, entry := range os.Environ() { + parts := strings.SplitN(entry, "=", 2) + if len(parts) == 2 { + values[parts[0]] = parts[1] + } + } + result, err := json.MarshalIndent(values, "", " ") + pretty.Guard(err == nil, 1, "Fail: %v", err) + + fmt.Fprintf(os.Stdout, "``` env dump %q begins\n%s\n```\n", preformatLabel, result) + }, +} + +func init() { + internalCmd.AddCommand(internalEnvCmd) + internalEnvCmd.Flags().StringVarP(&preformatLabel, "label", "l", "", "Label to identitfy variable dump.") + internalEnvCmd.MarkFlagRequired("label") +} diff --git a/common/version.go b/common/version.go index 3ca9a910..95856232 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.6.2` + Version = `v9.7.0` ) diff --git a/conda/activate.go b/conda/activate.go new file mode 100644 index 00000000..b46c7f68 --- /dev/null +++ b/conda/activate.go @@ -0,0 +1,172 @@ +package conda + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/common" +) + +const ( + preformatMarker = "```" + activateFile = "rcc_activate.json" +) + +func BinRcc() string { + self, err := os.Executable() + if err != nil { + return os.Args[0] + } + return self +} + +func capturePreformatted(incoming string) ([]string, string) { + lines := strings.SplitAfter(incoming, "\n") + capture := false + result := make([]string, 0, 5) + pending := make([]string, 0, len(lines)) + other := make([]string, 0, len(lines)) + for _, line := range lines { + flat := strings.TrimSpace(line) + if strings.HasPrefix(flat, preformatMarker) { + if len(pending) > 0 { + result = append(result, strings.Join(pending, "")) + pending = make([]string, 0, len(lines)) + } + capture = !capture + continue + } + if !capture { + other = append(other, line) + continue + } + pending = append(pending, line) + } + if len(pending) > 0 { + result = append(result, strings.Join(pending, "")) + } + return result, strings.Join(other, "") +} + +func createScript(targetFolder string) (string, error) { + script := template.New("script") + script, err := script.Parse(activateScript) + if err != nil { + return "", err + } + details := make(map[string]string) + details["Rcc"] = BinRcc() + details["Robocorphome"] = RobocorpHome() + details["Micromamba"] = BinMicromamba() + details["Live"] = targetFolder + buffer := bytes.NewBuffer(nil) + script.Execute(buffer, details) + + scriptfile := filepath.Join(targetFolder, fmt.Sprintf("rcc_activate%s", commandSuffix)) + err = ioutil.WriteFile(scriptfile, buffer.Bytes(), 0o755) + if err != nil { + return "", err + } + return scriptfile, nil +} + +func parseJson(content string) (map[string]string, error) { + result := make(map[string]string) + err := json.Unmarshal([]byte(content), &result) + return result, err +} + +func diffStringMaps(before, after map[string]string) map[string]string { + result := make(map[string]string) + for key, _ := range before { + _, ok := after[key] + if !ok { + result[key] = "" + } + } + for key, past := range before { + future, ok := after[key] + if ok && past != future { + result[key] = future + } + } + for key, value := range after { + _, ok := before[key] + if !ok { + result[key] = value + } + } + return result +} + +func Activate(sink *os.File, targetFolder string) error { + envCommand := []string{BinRcc(), "internal", "env", "--label", "before"} + out, _, err := LiveCapture(targetFolder, envCommand...) + if err != nil { + return err + } + parts, _ := capturePreformatted(out) + if len(parts) == 0 { + return fmt.Errorf("Could not detect environment details from 'before' output.") + } + before, err := parseJson(parts[0]) + if err != nil { + return err + } + + script, err := createScript(targetFolder) + if err != nil { + return err + } + + out, _, err = LiveCapture(targetFolder, script) + if err != nil { + fmt.Fprintf(sink, "%v\n%s\n", err, out) + return err + } + parts, other := capturePreformatted(out) + fmt.Fprintf(sink, "%s\n", other) + if len(parts) == 0 { + return fmt.Errorf("Could not detect environment details from 'after' output.") + } + after, err := parseJson(parts[0]) + if err != nil { + return err + } + difference := diffStringMaps(before, after) + body, err := json.MarshalIndent(difference, "", " ") + if err != nil { + return err + } + targetJson := filepath.Join(targetFolder, activateFile) + err = ioutil.WriteFile(targetJson, body, 0o644) + if err != nil { + return err + } + return nil +} + +func LoadActivationEnvironment(targetFolder string) []string { + result := []string{} + targetJson := filepath.Join(targetFolder, activateFile) + content, err := ioutil.ReadFile(targetJson) + if err != nil { + return result + } + var entries map[string]string + err = json.Unmarshal(content, &entries) + if err != nil { + return result + } + for name, value := range entries { + result = append(result, fmt.Sprintf("%s=%s", name, value)) + } + common.Trace("Environment activation added %d variables.", len(result)) + return result +} diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index b12664b2..1601c7a7 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -10,6 +10,13 @@ const ( Newline = "\n" defaultRobocorpLocation = "$HOME/.robocorp" binSuffix = "/bin" + activateScript = `#!/bin/bash + +export MAMBA_ROOT_PREFIX={{.Robocorphome}} +eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" +{{.Rcc}} internal env -l after +` + commandSuffix = ".sh" ) var ( diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 5ceb975c..95f82f0a 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -10,6 +10,13 @@ const ( Newline = "\n" defaultRobocorpLocation = "$HOME/.robocorp" binSuffix = "/bin" + activateScript = `#!/bin/bash + +export MAMBA_ROOT_PREFIX={{.Robocorphome}} +eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" +{{.Rcc}} internal env -l after +` + commandSuffix = ".sh" ) var ( diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index db2d4a1f..947daf92 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -21,6 +21,11 @@ const ( scriptSuffix = "\\Scripts" usrSuffix = "\\usr" binSuffix = "\\bin" + activateScript = "@echo off\n" + + "set MAMBA_ROOT_PREFIX=\"{{.Robocorphome}}\"\n" + + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell -s cmd.exe activate -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + + "call \"{{.Rcc}}\" internal env -l after\n" + commandSuffix = ".cmd" ) func MicromambaLink() string { diff --git a/conda/robocorp.go b/conda/robocorp.go index 48a0c0fc..fa56b15a 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -173,7 +173,7 @@ func EnvironmentExtensionFor(location string) []string { return append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, - "CONDA_PROMPT_MODIFIER=(rcc)", + "CONDA_PROMPT_MODIFIER=(rcc) ", "CONDA_SHLVL=1", "PYTHONHOME=", "PYTHONSTARTUP=", diff --git a/conda/workflows.go b/conda/workflows.go index c94d8b93..cf889709 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "errors" "fmt" + "io" "io/ioutil" "math/rand" "os" @@ -79,17 +80,35 @@ func reuseExistingLive(key string) (bool, error) { return false, nil } -func LiveExecution(liveFolder string, command ...string) error { +func livePrepare(liveFolder string, command ...string) (*shell.Task, error) { searchPath := FindPath(liveFolder) commandName := command[0] task, ok := searchPath.Which(commandName, FileExtensions) if !ok { - return fmt.Errorf("Cannot find command: %v", commandName) + return nil, fmt.Errorf("Cannot find command: %v", commandName) } common.Debug("Using %v as command %v.", task, commandName) command[0] = task environment := EnvironmentFor(liveFolder) - _, err := shell.New(environment, ".", command...).StderrOnly().Transparent() + return shell.New(environment, ".", command...), nil +} + +func LiveCapture(liveFolder string, command ...string) (string, int, error) { + task, err := livePrepare(liveFolder, command...) + if err != nil { + return "", 9999, err + } + return task.CaptureOutput() +} + +func LiveExecution(sink *os.File, liveFolder string, command ...string) error { + defer sink.Sync() + fmt.Fprintf(sink, "Command %q at %q:\n", command, liveFolder) + task, err := livePrepare(liveFolder, command...) + if err != nil { + return err + } + _, err = task.Tracked(sink, false) return err } @@ -148,19 +167,35 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) + planfile := fmt.Sprintf("%s.plan", targetFolder) + planWriter, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return false, false + } + defer func() { + planWriter.Close() + os.Remove(planfile) + }() + fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall) + stopwatch := common.Stopwatch("installation plan") + fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) + fmt.Fprintf(planWriter, "%s\n", yaml) + common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) ttl := "57600" if force { ttl = "0" } command := []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} - if common.DebugFlag { + if true || common.DebugFlag { command = []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") common.Timeline("Micromamba start.") - code, err := shell.New(CondaEnvironment(), ".", command...).StderrOnly().Observed(observer, false) + fmt.Fprintf(planWriter, "\n--- micromamba plan @%ss ---\n\n", stopwatch) + tee := io.MultiWriter(observer, planWriter) + code, err := shell.New(CondaEnvironment(), ".", command...).Tracked(tee, false) if err != nil || code != 0 { common.Timeline("micromamba fail.") common.Fatal("Micromamba", err) @@ -170,6 +205,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if observer.HasFailures(targetFolder) { return false, true } + fmt.Fprintf(planWriter, "\n--- pip plan @%ss ---\n\n", stopwatch) pipCache, wheelCache := PipCache(), WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { @@ -180,11 +216,11 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet"} - if common.DebugFlag { + if true || common.DebugFlag { pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText} } common.Debug("=== new live --- pip install phase ===") - err = LiveExecution(targetFolder, pipCommand...) + err = LiveExecution(planWriter, targetFolder, pipCommand...) if err != nil { common.Timeline("pip fail.") common.Fatal("Pip", err) @@ -192,6 +228,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } common.Timeline("pip done.") } + fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { common.Timeline("post install.") common.Debug("=== new live --- post install phase ===") @@ -202,8 +239,8 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Log("%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) return false, false } - common.Log("Running post install script '%s' ...", script) - err = LiveExecution(targetFolder, scriptCommand...) + common.Debug("Running post install script '%s' ...", script) + err = LiveExecution(planWriter, targetFolder, scriptCommand...) if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) @@ -211,6 +248,21 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } } } + common.Debug("=== new live --- activate phase ===") + fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) + err = Activate(planWriter, targetFolder) + if err != nil { + common.Log("%sActivation failure: %v%s", pretty.Yellow, err, pretty.Reset) + } + for _, line := range LoadActivationEnvironment(targetFolder) { + fmt.Fprintf(planWriter, "%s\n", line) + } + fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) + planWriter.Sync() + planWriter.Close() + finalplan := filepath.Join(targetFolder, "rcc_plan.log") + os.Rename(planfile, finalplan) + common.Log("%sInstallation plan is: %v%s", pretty.Yellow, finalplan, pretty.Reset) common.Debug("=== new live --- finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") diff --git a/docs/changelog.md b/docs/changelog.md index abd184a6..3c463a2d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v9.7.0 (date: 10.3.2021) + +- conda environments are now activated once on creation, and variables go + with environment, as `rcc_activate.json` +- there is also now new "installation plan" file inside environment, called + `rcc_plan.log` which contains events that lead to activation +- normal runs are now more silent, since details are moved into "plan" file + ## v9.6.2 (date: 5.3.2021) - fix for time formats used in timeline, some metrics, and stopwatch diff --git a/operations/running.go b/operations/running.go index 5c1dad11..60129171 100644 --- a/operations/running.go +++ b/operations/running.go @@ -182,6 +182,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } + environment = append(environment, conda.LoadActivationEnvironment(label)...) common.Debug("DEBUG: about to run command - %v", task) _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) after := make(map[string]string) diff --git a/robot/robot.go b/robot/robot.go index 4342d306..4773aebe 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -321,7 +321,7 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo return append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, - "CONDA_PROMPT_MODIFIER=(rcc)", + "CONDA_PROMPT_MODIFIER=(rcc) ", "CONDA_SHLVL=1", "PYTHONHOME=", "PYTHONSTARTUP=", diff --git a/shell/task.go b/shell/task.go index ac5007b2..307ffecf 100644 --- a/shell/task.go +++ b/shell/task.go @@ -106,6 +106,14 @@ func (it *Task) Observed(sink io.Writer, interactive bool) (int, error) { return it.execute(stdin, stdout, stderr) } +func (it *Task) Tracked(sink io.Writer, interactive bool) (int, error) { + var stdin io.Reader = os.Stdin + if !interactive { + stdin = bytes.NewReader([]byte{}) + } + return it.execute(stdin, sink, sink) +} + func (it *Task) CaptureOutput() (string, int, error) { stdin := bytes.NewReader([]byte{}) stdout := bytes.NewBuffer(nil) From 6df4361427ba36a59e00cf7e2f680313ef472c4a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 10 Mar 2021 14:51:54 +0200 Subject: [PATCH 083/516] RCC-161: micromamba upgrade (v9.7.1) - fixes/improvements to activation and installation plan - added missing content type to assistant requests - micromamba upgrade to 0.8.0 --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 6 ++++-- conda/workflows.go | 6 +++--- docs/changelog.md | 6 ++++++ operations/assistant.go | 2 ++ operations/running.go | 1 - robot/robot.go | 4 +++- 10 files changed, 22 insertions(+), 11 deletions(-) diff --git a/common/version.go b/common/version.go index 95856232..7095af7c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.7.0` + Version = `v9.7.1` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 1601c7a7..ee639fe4 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -51,7 +51,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.14/macos64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.8.0/macos64/micromamba" } func IsPosix() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 95f82f0a..cc2f6790 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -25,7 +25,7 @@ var ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.14/linux64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.8.0/linux64/micromamba" } func ExpandPath(entry string) string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 947daf92..0e26e78e 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -29,7 +29,7 @@ const ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.7.14/windows64/micromamba.exe" + return "https://downloads.robocorp.com/micromamba/v0.8.0/windows64/micromamba.exe" } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index fa56b15a..3cefa629 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -170,7 +170,7 @@ func EnvironmentExtensionFor(location string) []string { if ok { environment = append(environment, "PYTHON_EXE="+python) } - return append(environment, + environment = append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, "CONDA_PROMPT_MODIFIER=(rcc) ", @@ -188,6 +188,8 @@ func EnvironmentExtensionFor(location string) []string { searchPath.AsEnvironmental("PATH"), PythonPath().AsEnvironmental("PYTHONPATH"), ) + environment = append(environment, LoadActivationEnvironment(location)...) + return environment } func EnvironmentFor(location string) []string { @@ -239,7 +241,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 7014 + goodEnough := version >= 8000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/conda/workflows.go b/conda/workflows.go index cf889709..7e7147dd 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -176,7 +176,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh planWriter.Close() os.Remove(planfile) }() - fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall) + fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) stopwatch := common.Stopwatch("installation plan") fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) fmt.Fprintf(planWriter, "%s\n", yaml) @@ -186,9 +186,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - command := []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} + command := []string{BinMicromamba(), "create", "--always-copy", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} if true || common.DebugFlag { - command = []string{BinMicromamba(), "create", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--always-copy", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 3c463a2d..c2e915ec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.7.1 (date: 10.3.2021) + +- fixes/improvements to activation and installation plan +- added missing content type to assistant requests +- micromamba upgrade to 0.8.0 + ## v9.7.0 (date: 10.3.2021) - conda environments are now activated once on creation, and variables go diff --git a/operations/assistant.go b/operations/assistant.go index 0cb16239..52af67fd 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -285,6 +285,7 @@ func StopAssistantRun(client cloud.Client, account *account, workspaceId, assist token["error"] = reason request := client.NewRequest(fmt.Sprintf(stopAssistantApi, workspaceId, assistantId, runId)) request.Headers[authorization] = WorkspaceToken(credentials) + request.Headers[contentType] = applicationJson blob, err := json.Marshal(token) if err == nil { request.Body = bytes.NewReader(blob) @@ -308,6 +309,7 @@ func StartAssistantRun(client cloud.Client, account *account, workspaceId, assis } request := client.NewRequest(fmt.Sprintf(startAssistantApi, workspaceId, assistantId)) request.Headers[authorization] = WorkspaceToken(credentials) + request.Headers[contentType] = applicationJson request.Body, err = key.RequestBody(nil) if err != nil { return nil, err diff --git a/operations/running.go b/operations/running.go index 60129171..5c1dad11 100644 --- a/operations/running.go +++ b/operations/running.go @@ -182,7 +182,6 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } - environment = append(environment, conda.LoadActivationEnvironment(label)...) common.Debug("DEBUG: about to run command - %v", task) _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) after := make(map[string]string) diff --git a/robot/robot.go b/robot/robot.go index 4773aebe..3bb12ffa 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -318,7 +318,7 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo if ok { environment = append(environment, "PYTHON_EXE="+python) } - return append(environment, + environment = append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, "CONDA_PROMPT_MODIFIER=(rcc) ", @@ -338,6 +338,8 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory()), fmt.Sprintf("ROBOT_ARTIFACTS=%s", it.ArtifactDirectory()), ) + environment = append(environment, conda.LoadActivationEnvironment(location)...) + return environment } func (it *task) WorkingDirectory(robot Robot) string { From 94e30fa5ccf675df2f4d64b5c7527e0ed561be9a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 11 Mar 2021 15:14:44 +0200 Subject: [PATCH 084/516] RCC-150: installation plan improvements (v9.7.2) - adding visibility of installation plans in environment listing - added --json support to environment listing including installation plan file - added command `rcc env plan` to show installation plans for environment - installation plan is now also part of robot diagnostics, if available --- cmd/delete.go | 10 ++++----- cmd/diagnostics.go | 2 +- cmd/envPlan.go | 34 ++++++++++++++++++++++++++++++ cmd/list.go | 44 ++++++++++++++++++++++++++++++++++----- common/version.go | 2 +- conda/robocorp.go | 5 +++++ conda/workflows.go | 16 ++++++++++++-- docs/changelog.md | 7 +++++++ operations/diagnostics.go | 6 +++--- operations/issues.go | 16 ++++++++------ robot/robot.go | 14 +++++++++++++ robot_tests/fullrun.robot | 7 ++++++- 12 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 cmd/envPlan.go diff --git a/cmd/delete.go b/cmd/delete.go index 64bf0c2b..08529dce 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -14,11 +14,11 @@ var deleteCmd = &cobra.Command{ Long: `Delete the given virtual environment from existence. After deletion, it will not be available anymore.`, Run: func(cmd *cobra.Command, args []string) { - for _, label := range args { - common.Log("Removing %v", label) - err := conda.RemoveEnvironment(label) - if err != nil { - pretty.Exit(1, "Error: %v", err) + for _, prefix := range args { + for _, label := range conda.FindEnvironment(prefix) { + common.Log("Removing %v", label) + err := conda.RemoveEnvironment(label) + pretty.Guard(err == nil, 1, "Error: %v", err) } } }, diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index c886c7c1..bbe19cdf 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -21,7 +21,7 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - err := operations.PrintDiagnostics(fileOption, robotOption, jsonFlag) + _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/cmd/envPlan.go b/cmd/envPlan.go new file mode 100644 index 00000000..270c6eaa --- /dev/null +++ b/cmd/envPlan.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var envPlanCmd = &cobra.Command{ + Use: "plan", + Short: "Show installation plan for given environment (or prefix)", + Long: "Show installation plan for given environment (or prefix)", + + Run: func(cmd *cobra.Command, args []string) { + for _, prefix := range args { + for _, label := range conda.FindEnvironment(prefix) { + planfile, ok := conda.InstallationPlan(label) + pretty.Guard(ok, 1, "Could not find plan for: %v", label) + content, err := ioutil.ReadFile(planfile) + pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) + fmt.Fprintf(os.Stdout, string(content)) + } + } + }, +} + +func init() { + envCmd.AddCommand(envPlanCmd) +} diff --git a/cmd/list.go b/cmd/list.go index 2964b7cb..f2cf1955 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,7 +1,9 @@ package cmd import ( + "encoding/json" "fmt" + "os" "sort" "time" @@ -12,6 +14,20 @@ import ( "github.com/spf13/cobra" ) +func textDump(lines []string) { + sort.Strings(lines) + for _, line := range lines { + common.Log("%s", line) + } +} + +func jsonDump(entries map[string]interface{}) { + body, err := json.MarshalIndent(entries, "", " ") + if err == nil { + fmt.Fprintln(os.Stdout, string(body)) + } +} + var listCmd = &cobra.Command{ Use: "list", Short: "Listing currently managed virtual environments.", @@ -23,8 +39,13 @@ in human readable form.`, pretty.Exit(1, "No environments available.") } lines := make([]string, 0, len(templates)) - common.Log("%-25s %-25s %-16s %s", "Last used", "Last cloned", "Environment", "Leased duration") + entries := make(map[string]interface{}) + if !jsonFlag { + common.Log("%-25s %-25s %-16s %5s %s", "Last used", "Last cloned", "Environment", "Plan?", "Leased duration") + } for _, template := range templates { + details := make(map[string]interface{}) + entries[template] = details cloned := "N/A" used := cloned when, err := conda.LastUsed(conda.TemplateFrom(template)) @@ -35,15 +56,28 @@ in human readable form.`, if err == nil { used = when.Format(time.RFC3339) } - lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %q %s", used, cloned, template, conda.WhoLeased(template), conda.LeaseExpires(template))) + details["name"] = template + details["used"] = used + details["cloned"] = cloned + details["leased"] = conda.WhoLeased(template) + details["expires"] = conda.LeaseExpires(template) + details["base"] = conda.TemplateFrom(template) + details["live"] = conda.LiveFrom(template) + planfile, plan := conda.InstallationPlan(template) + lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %5v %q %s", used, cloned, template, plan, conda.WhoLeased(template), conda.LeaseExpires(template))) + if plan { + details["plan"] = planfile + } } - sort.Strings(lines) - for _, line := range lines { - common.Log("%s", line) + if jsonFlag { + jsonDump(entries) + } else { + textDump(lines) } }, } func init() { envCmd.AddCommand(listCmd) + listCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") } diff --git a/common/version.go b/common/version.go index 7095af7c..e9a281c5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.7.1` + Version = `v9.7.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 3cefa629..23347ef2 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -342,3 +342,8 @@ func OrphanList() []string { result = append(result, orphansFrom(LiveLocation())...) return result } + +func InstallationPlan(hash string) (string, bool) { + finalplan := filepath.Join(LiveFrom(hash), "rcc_plan.log") + return finalplan, pathlib.IsFile(finalplan) +} diff --git a/conda/workflows.go b/conda/workflows.go index 7e7147dd..1c0ecfe8 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -260,7 +260,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planWriter.Sync() planWriter.Close() - finalplan := filepath.Join(targetFolder, "rcc_plan.log") + finalplan, _ := InstallationPlan(key) os.Rename(planfile, finalplan) common.Log("%sInstallation plan is: %v%s", pretty.Yellow, finalplan, pretty.Reset) common.Debug("=== new live --- finalize phase ===") @@ -301,11 +301,11 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. if err != nil { return "", "", nil, err } - common.Log("FINAL union conda environment descriptior:\n---\n%v---", yaml) hash := shortDigest(yaml) if !save { return hash, yaml, right, nil } + common.Log("FINAL union conda environment descriptior:\n---\n%v---", yaml) err = right.SaveAsRequirements(requirementsText) if err != nil { return "", "", nil, err @@ -453,6 +453,18 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return "", errors.New("Could not create environment.") } +func FindEnvironment(prefix string) []string { + prefix = strings.ToLower(prefix) + livelist := LiveList() + result := make([]string, 0, len(livelist)) + for _, entry := range livelist { + if strings.HasPrefix(entry, prefix) { + result = append(result, entry) + } + } + return result +} + func RemoveEnvironment(label string) error { if IsLeasedEnvironment(label) { return fmt.Errorf("WARNING: %q is leased by %q and wont be deleted!", label, WhoLeased(label)) diff --git a/docs/changelog.md b/docs/changelog.md index c2e915ec..2d35a1d8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.7.2 (date: 11.3.2021) + +- adding visibility of installation plans in environment listing +- added --json support to environment listing including installation plan file +- added command `rcc env plan` to show installation plans for environment +- installation plan is now also part of robot diagnostics, if available + ## v9.7.1 (date: 10.3.2021) - fixes/improvements to activation and installation plan diff --git a/operations/diagnostics.go b/operations/diagnostics.go index d4ad8f95..c7b5a578 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -219,10 +219,10 @@ func fileIt(filename string) (io.WriteCloser, error) { return file, nil } -func PrintDiagnostics(filename, robotfile string, json bool) error { +func ProduceDiagnostics(filename, robotfile string, json bool) (*common.DiagnosticStatus, error) { file, err := fileIt(filename) if err != nil { - return err + return nil, err } defer file.Close() result := RunDiagnostics() @@ -234,7 +234,7 @@ func PrintDiagnostics(filename, robotfile string, json bool) error { } else { humaneDiagnostics(file, result) } - return nil + return result, nil } type Unmarshaler func([]byte, interface{}) error diff --git a/operations/issues.go b/operations/issues.go index 072430fe..7f7b742c 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -47,13 +47,13 @@ func createIssueZip(attachmentsFiles []string) (string, error) { return zipfile, nil } -func createDiagnosticsReport(robotfile string) (string, error) { +func createDiagnosticsReport(robotfile string) (string, *common.DiagnosticStatus, error) { file := filepath.Join(conda.RobocorpTemp(), "diagnostics.txt") - err := PrintDiagnostics(file, robotfile, false) + diagnostics, err := ProduceDiagnostics(file, robotfile, false) if err != nil { - return "", err + return "", nil, err } - return file, nil + return file, diagnostics, nil } func virtualName(filename string) (string, error) { @@ -70,10 +70,14 @@ func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, if err != nil { return err } - diagnostics, err := createDiagnosticsReport(robotFile) + diagnostics, data, err := createDiagnosticsReport(robotFile) if err == nil { attachmentsFiles = append(attachmentsFiles, diagnostics) } + plan, ok := data.Details["robot-conda-plan"] + if ok { + attachmentsFiles = append(attachmentsFiles, plan) + } attachmentsFiles = append(attachmentsFiles, reportFile) filename, err := createIssueZip(attachmentsFiles) if err != nil { @@ -88,7 +92,7 @@ func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, token["account-email"] = email token["fileName"] = shortname token["controller"] = common.ControllerIdentity() - _, ok := token["platform"] + _, ok = token["platform"] if !ok { token["platform"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) } diff --git a/robot/robot.go b/robot/robot.go index 3bb12ffa..cbc9c20b 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -25,6 +25,7 @@ type Robot interface { TaskByName(string) Task UsesConda() bool CondaConfigFile() string + CondaHash() string RootDirectory() string Validate() (bool, error) Diagnostics(*common.DiagnosticStatus) @@ -164,6 +165,11 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus) { } target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) target.Details["robot-conda-file"] = it.CondaConfigFile() + target.Details["robot-conda-hash"] = it.CondaHash() + plan, ok := conda.InstallationPlan(it.CondaHash()) + if ok { + target.Details["robot-conda-plan"] = plan + } target.Details["robot-root-directory"] = it.RootDirectory() target.Details["robot-working-directory"] = it.WorkingDirectory() target.Details["robot-artifact-directory"] = it.ArtifactDirectory() @@ -262,6 +268,14 @@ func (it *robot) CondaConfigFile() string { return filepath.Join(it.Root, it.Conda) } +func (it *robot) CondaHash() string { + result, err := conda.CalculateComboHash(filepath.Join(it.Root, it.Conda)) + if err != nil { + return "" + } + return result +} + func (it *robot) WorkingDirectory() string { return it.Root } diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 2f0af3e9..8c3fd0ba 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -107,10 +107,15 @@ Using and running template example with shell file Use STDERR Must Have robotframework=3.1 vs. robotframework=3.2 - Goal Merge two different conda.yaml files with conflict fails + Goal Merge two different conda.yaml files without conflict passes Step build/rcc env new --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent Must Have 786f01e87dc8d6e6 + Goal Can list environments as JSON + Step build/rcc env list --controller citests --json + Must Have 786f01e87dc8d6e6 + Must Be Json Response + Goal See variables from specific environment without robot.yaml knowledge Step build/rcc env variables --controller citests conda/testdata/conda.yaml Must Have ROBOCORP_HOME= From 2c7aa23f82b1b8e25ab12dcdbb29515a6b12a956 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 16 Mar 2021 10:00:05 +0200 Subject: [PATCH 085/516] FIX: minor fixes (v9.7.3) - upgrading micromamba dependency to 0.8.2 version - added .robot, .csv, .yaml, .yml, and .json in non-executable fileset - also added "dot" files as non-executable - added timestamp update to copyfile functionality - added toplevel --tag option to allow semantic tagging for client applications to indicate meaning of rcc execution call --- cmd/root.go | 1 + common/variables.go | 1 + common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 9 +++++++++ operations/fixing.go | 7 ++++++- pathlib/copyfile.go | 8 +++++++- 10 files changed, 29 insertions(+), 7 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index b1a8df19..439ddceb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -77,6 +77,7 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP/rcc.yaml)") rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") diff --git a/common/variables.go b/common/variables.go index 122290d4..09a1dfa4 100644 --- a/common/variables.go +++ b/common/variables.go @@ -18,6 +18,7 @@ var ( ControllerType string LeaseContract string EnvironmentHash string + SemanticTag string When int64 Clock *stopwatch ) diff --git a/common/version.go b/common/version.go index e9a281c5..6d6ad609 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.7.2` + Version = `v9.7.3` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index ee639fe4..e5df42d0 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -51,7 +51,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.0/macos64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.8.2/macos64/micromamba" } func IsPosix() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index cc2f6790..16e02cec 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -25,7 +25,7 @@ var ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.0/linux64/micromamba" + return "https://downloads.robocorp.com/micromamba/v0.8.2/linux64/micromamba" } func ExpandPath(entry string) string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 0e26e78e..a7a0b19f 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -29,7 +29,7 @@ const ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.0/windows64/micromamba.exe" + return "https://downloads.robocorp.com/micromamba/v0.8.2/windows64/micromamba.exe" } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 23347ef2..04b665cf 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -241,7 +241,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 8000 + goodEnough := version >= 8002 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/docs/changelog.md b/docs/changelog.md index 2d35a1d8..bfa5bd56 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v9.7.3 (date: 16.3.2021) + +- upgrading micromamba dependency to 0.8.2 version +- added .robot, .csv, .yaml, .yml, and .json in non-executable fileset +- also added "dot" files as non-executable +- added timestamp update to copyfile functionality +- added toplevel --tag option to allow semantic tagging for client + applications to indicate meaning of rcc execution call + ## v9.7.2 (date: 11.3.2021) - adding visibility of installation plans in environment listing diff --git a/operations/fixing.go b/operations/fixing.go index f3587192..d2ed05da 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -21,6 +21,11 @@ func init() { nonExecutableExtensions[".txt"] = true nonExecutableExtensions[".htm"] = true nonExecutableExtensions[".html"] = true + nonExecutableExtensions[".csv"] = true + nonExecutableExtensions[".yml"] = true + nonExecutableExtensions[".yaml"] = true + nonExecutableExtensions[".json"] = true + nonExecutableExtensions[".robot"] = true } func ToUnix(content []byte) []byte { @@ -43,7 +48,7 @@ func fixShellFile(fullpath string) { func makeExecutable(fullpath string, file os.FileInfo) { extension := strings.ToLower(filepath.Ext(file.Name())) ignore, ok := nonExecutableExtensions[extension] - if ok && ignore || file.Mode() == 0o755 { + if ok && ignore || file.Mode() == 0o755 || strings.HasPrefix(file.Name(), ".") { return } os.Chmod(fullpath, 0o755) diff --git a/pathlib/copyfile.go b/pathlib/copyfile.go index 77044e0a..fc8290ba 100644 --- a/pathlib/copyfile.go +++ b/pathlib/copyfile.go @@ -41,7 +41,13 @@ func RestoreFile(source, target string, overwrite bool) error { } func CopyFile(source, target string, overwrite bool) error { - return copyFile(source, target, overwrite, io.Copy) + mark, err := Modtime(source) + if err != nil { + return err + } + err = copyFile(source, target, overwrite, io.Copy) + TouchWhen(target, mark) + return err } func copyFile(source, target string, overwrite bool, copier copyfunc) error { From c6f4a04b32ddda5e5148f63d421b0e92cf26d0a8 Mon Sep 17 00:00:00 2001 From: Janne Aukia Date: Tue, 16 Mar 2021 10:58:28 +0200 Subject: [PATCH 086/516] Fix for a small typo --- conda/workflows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/workflows.go b/conda/workflows.go index 1c0ecfe8..32d24947 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -305,7 +305,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. if !save { return hash, yaml, right, nil } - common.Log("FINAL union conda environment descriptior:\n---\n%v---", yaml) + common.Log("FINAL union conda environment descriptor:\n---\n%v---", yaml) err = right.SaveAsRequirements(requirementsText) if err != nil { return "", "", nil, err From e8726ef1fa1930c9bca89efc9d5becfaf92902cc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Mar 2021 13:15:33 +0200 Subject: [PATCH 087/516] FIX: pull request and micromamba no-rc (v9.7.4) - typo fix pull request from jaukia - added micromamba --no-rc flag --- common/version.go | 2 +- conda/workflows.go | 4 ++-- docs/changelog.md | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 6d6ad609..dc52e4d7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.7.3` + Version = `v9.7.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 32d24947..5857bbe6 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -186,9 +186,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - command := []string{BinMicromamba(), "create", "--always-copy", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} + command := []string{BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} if true || common.DebugFlag { - command = []string{BinMicromamba(), "create", "--always-copy", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} + command = []string{BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} } observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index bfa5bd56..bcc724ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.7.4 (date: 17.3.2021) + +- typo fix pull request from jaukia +- added micromamba --no-rc flag + ## v9.7.3 (date: 16.3.2021) - upgrading micromamba dependency to 0.8.2 version From 7d7131f4d8e288e87b7b4f38de5420d983e804fb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 18 Mar 2021 12:03:50 +0200 Subject: [PATCH 088/516] RCC-158: settings.yaml (v9.8.0) - ALPHA level pre-release with settings.yaml (do not use, unless you know what you are doing) - started to moved some of hardcoded things into settings.yaml (not used yet) - minor assistant upload fix, where one error case was not marked as error --- Rakefile | 2 +- assets/settings.yaml | 39 +++++++++++++ blobs/asset_test.go | 1 + cmd/env.go | 7 ++- cmd/settings.go | 34 +++++++++++ common/version.go | 2 +- docs/changelog.md | 7 +++ operations/assistant.go | 1 + operations/issues.go | 11 ++++ operations/settings.go | 89 +++++++++++++++++++++++++++++ operations/zipper.go | 12 ++++ settings/data.go | 121 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 assets/settings.yaml create mode 100644 cmd/settings.go create mode 100644 operations/settings.go create mode 100644 settings/data.go diff --git a/Rakefile b/Rakefile index efc53b93..29a34715 100644 --- a/Rakefile +++ b/Rakefile @@ -30,7 +30,7 @@ task :assets do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.zip assets/man/* docs/changelog.md" + sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md" end task :support do diff --git a/assets/settings.yaml b/assets/settings.yaml new file mode 100644 index 00000000..6d98ba5a --- /dev/null +++ b/assets/settings.yaml @@ -0,0 +1,39 @@ +endpoints: + cloud-api: https://api.eu1.robocorp.com + cloud-linking: https://id.robocorp.com + pypi: https://pypi.org/simple/ + conda: https://repo.anaconda.org + downloads: https://downloads.robocorp.com + docs: https://robocorp.com/docs/ + telemetry: https://telemetry.robocorp.com + issues: https://telemetry.robocorp.com + portal: https://robocorp.com/portal/ + robot-pull: https://github.com + +proxies: + http: + https: + +autoupdates: + assistant: https://downloads.robocorp.com/assistant/releases + workforce-agent: https://downloads.robocorp.com/agent/releases + lab: https://downloads.code.robocorp.com/lab/installer + +certificates: + ignore-ssl-verification: false + root-location: + +logs: + level: debug + root-location: "%localappdata%\\robocorp\\logs" + +business-data: + root-location: "%HOMEDRIVE%%HOMEPATH%\\Documents" + +branding: + logo: https://downloads.robocorp.com/company/press-kit/logos/robocorp-logo-black.svg + theme-color: FF0000 + +meta: + source: builtin + version: 2021.03 diff --git a/blobs/asset_test.go b/blobs/asset_test.go index fad32bc3..27429e3c 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -14,6 +14,7 @@ func TestCanSeeBaseZipAsset(t *testing.T) { must_be.Panic(func() { blobs.MustAsset("assets/missing.zip") }) wont_be.Panic(func() { blobs.MustAsset("assets/standard.zip") }) wont_be.Panic(func() { blobs.MustAsset("assets/python.zip") }) + wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) _, err := blobs.Asset("assets/missing.zip") wont_be.Nil(err) diff --git a/cmd/env.go b/cmd/env.go index e07aa52b..dd4d0816 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" "github.com/spf13/cobra" ) @@ -9,11 +10,15 @@ var envCmd = &cobra.Command{ Use: "env", Aliases: []string{"environment", "e"}, Short: "Group of commands related to `environment management`.", - Long: `This "env" command set is for managing virtual environments + Long: `This "env" command set is for managing virtual environments, used in task context locally.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + operations.CriticalEnvironmentSettingsCheck() + }, } func init() { + rootCmd.AddCommand(envCmd) envCmd.PersistentFlags().StringVar(&common.LeaseContract, "lease", "", "unique lease contract for long living environments") envCmd.PersistentFlags().StringVar(&common.StageFolder, "stage", "", "internal, DO NOT USE (unless you know what you are doing)") diff --git a/cmd/settings.go b/cmd/settings.go new file mode 100644 index 00000000..75251bc0 --- /dev/null +++ b/cmd/settings.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var settingsCmd = &cobra.Command{ + Use: "settings", + Short: "Show default settings.yaml content.", + Long: "Show default settings.yaml content.", + Run: func(cmd *cobra.Command, args []string) { + if jsonFlag { + config, err := operations.SummonSettings() + pretty.Guard(err == nil, 2, "Error while loading settings: %v", err) + json, err := config.AsJson() + pretty.Guard(err == nil, 3, "Error while converting settings: %v", err) + fmt.Fprintf(os.Stdout, "%s", string(json)) + } else { + raw, err := operations.DefaultSettings() + pretty.Guard(err == nil, 1, "Error while loading defaults: %v", err) + fmt.Fprintf(os.Stdout, "%s", string(raw)) + } + }, +} + +func init() { + configureCmd.AddCommand(settingsCmd) + settingsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show effective settings as JSON stream.") +} diff --git a/common/version.go b/common/version.go index dc52e4d7..20a52120 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.7.4` + Version = `v9.8.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index bcc724ab..0224bd54 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.8.0 (date: 18.3.2021) + +- ALPHA level pre-release with settings.yaml (do not use, unless you know + what you are doing) +- started to moved some of hardcoded things into settings.yaml (not used yet) +- minor assistant upload fix, where one error case was not marked as error + ## v9.7.4 (date: 17.3.2021) - typo fix pull request from jaukia diff --git a/operations/assistant.go b/operations/assistant.go index 52af67fd..402090c8 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -127,6 +127,7 @@ func (it *ArtifactPublisher) Publish(fullpath, relativepath string, details os.F return //err } if response.Status < 200 || 299 < response.Status { + it.ErrorCount += 1 common.Log("ERR: status code %v", response.Status) return //err } diff --git a/operations/issues.go b/operations/issues.go index 7f7b742c..93bfb762 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -44,6 +44,17 @@ func createIssueZip(attachmentsFiles []string) (string, error) { niceName := fmt.Sprintf("%x_%s", index+1, filepath.Base(attachment)) zipper.Add(attachment, niceName, nil) } + // getting settings.yaml is optional, it should not break issue reporting + config, err := SummonSettings() + if err != nil { + return zipfile, nil + } + blob, err := config.AsYaml() + if err != nil { + return zipfile, nil + } + niceName := fmt.Sprintf("%x_settings.yaml", len(attachmentsFiles)+1) + zipper.AddBlob(niceName, blob) return zipfile, nil } diff --git a/operations/settings.go b/operations/settings.go new file mode 100644 index 00000000..10fe5181 --- /dev/null +++ b/operations/settings.go @@ -0,0 +1,89 @@ +package operations + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" +) + +var ( + cachedSettings *settings.Settings +) + +func cacheSettings(result *settings.Settings) (*settings.Settings, error) { + if result != nil { + cachedSettings = result + } + return result, nil +} + +func SettingsFileLocation() string { + return filepath.Join(conda.RobocorpHome(), "settings.yaml") +} + +func HasCustomSettings() bool { + return pathlib.IsFile(SettingsFileLocation()) +} + +func DefaultSettings() ([]byte, error) { + return blobs.Asset("assets/settings.yaml") +} + +func rawSettings() (content []byte, location string, err error) { + if HasCustomSettings() { + location = SettingsFileLocation() + content, err = ioutil.ReadFile(location) + return content, location, err + } else { + content, err = DefaultSettings() + return content, "builtin", err + } +} + +func SummonSettings() (*settings.Settings, error) { + if cachedSettings != nil { + return cachedSettings, nil + } + content, source, err := rawSettings() + if err != nil { + return nil, err + } + config, err := settings.FromBytes(content) + if err != nil { + return nil, err + } + return cacheSettings(config.Source(source)) +} + +func DefaultEndpoint() (string, error) { + config, err := SummonSettings() + if err != nil { + return "", err + } + endpoints := config.Endpoints + if endpoints == nil { + return "", fmt.Errorf("Brokens settings: all endpoints are missing!") + } + return "", nil +} + +func CriticalEnvironmentSettingsCheck() { + return + // JIPPO:FIXME:JIPPO -- continue here + config, err := SummonSettings() + pretty.Guard(err == nil, 80, "Aborting! Could not even get setting, reason: %v", err) + result := &common.DiagnosticStatus{ + Details: make(map[string]string), + Checks: []*common.DiagnosticCheck{}, + } + config.Diagnostics(result) + humaneDiagnostics(os.Stderr, result) +} diff --git a/operations/zipper.go b/operations/zipper.go index 74c67db9..80464e32 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -195,6 +195,18 @@ func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { } } +func (it *zipper) AddBlob(relativepath string, blob []byte) { + target, err := it.writer.Create(relativepath) + if err != nil { + it.Note(err) + return + } + _, err = target.Write(blob) + if err != nil { + it.Note(err) + } +} + func (it *zipper) Close() { err := it.writer.Close() if err != nil { diff --git a/settings/data.go b/settings/data.go new file mode 100644 index 00000000..aee8795e --- /dev/null +++ b/settings/data.go @@ -0,0 +1,121 @@ +package settings + +import ( + "encoding/json" + + "github.com/robocorp/rcc/common" + "gopkg.in/yaml.v1" +) + +type StringMap map[string]string + +type Settings struct { + Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` + Branding StringMap `yaml:"branding" json:"branding"` + BusinessData *BusinessData `yaml:"business-data" json:"business-data"` + Certificates *Certificates `yaml:"certificates" json:"certificates"` + Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` + Logs *Logs `yaml:"logs" json:"logs"` + Meta *Meta `yaml:"meta" json:"meta"` + Proxies *Proxies `yaml:"proxies" json:"proxies"` +} + +func FromBytes(raw []byte) (*Settings, error) { + var settings Settings + err := yaml.Unmarshal(raw, &settings) + if err != nil { + return nil, err + } + return &settings, nil +} + +func (it *Settings) AsYaml() ([]byte, error) { + content, err := yaml.Marshal(it) + if err != nil { + return nil, err + } + return content, nil +} + +func (it *Settings) Source(filename string) *Settings { + if it.Meta != nil && len(filename) > 0 { + it.Meta.Source = filename + } + return it +} + +func (it *Settings) AsJson() ([]byte, error) { + content, err := json.MarshalIndent(it, "", " ") + if err != nil { + return nil, err + } + return content, nil +} + +func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { + diagnose := target.Diagnose("Settings") + correct := true + if it.BusinessData == nil { + diagnose.Warning("", "settings.yaml: business-data section is totally missing") + correct = false + } + if it.Certificates == nil { + diagnose.Warning("", "settings.yaml: certificates section is totally missing") + correct = false + } + if it.Endpoints == nil { + correct = false + } + if it.Logs == nil { + diagnose.Warning("", "settings.yaml: logs section is totally missing") + correct = false + } + if it.Meta == nil { + diagnose.Warning("", "settings.yaml: meta section is totally missing") + correct = false + } + if it.Proxies == nil { + diagnose.Warning("", "settings.yaml: proxies section is totally missing") + correct = false + } + if correct { + diagnose.Ok("Toplevel settings are ok.") + } +} + +type BusinessData struct { + RootLocation string `yaml:"root-location" json:"root-location"` +} + +type Certificates struct { + IgnoreVerification bool `yaml:"ignore-ssl-verification" json:"ignore-ssl-verification"` + RootLocation string `yaml:"root-location" json:"root-location"` +} + +type Endpoints struct { + CloudApi string `yaml:"cloud-api" json:"cloud-api"` + CloudLinking string `yaml:"cloud-linking" json:"cloud-linking"` + Conda string `yaml:"conda" json:"conda"` + Docs string `yaml:"docs" json:"docs"` + Downloads string `yaml:"downloads" json:"downloads"` + Issues string `yaml:"issues" json:"issues"` + Portal string `yaml:"portal" json:"portal"` + Pypi string `yaml:"pypi" json:"pypi"` + RobotPull string `yaml:"robot-pull" json:"robot-pull"` + Telemetry string `yaml:"telemetry" json:"telemetry"` +} + +type Logs struct { + Level string `yaml:"level" json:"level"` + RootLocation string `yaml:"root-location" json:"root-location"` +} + +type Meta struct { + Source string `yaml:"source" json:"source"` + Version string `yaml:"version" json:"version"` +} + +type Proxies struct { + Http string `yaml:"http" json:"http"` + Https string `yaml:"https" json:"https"` +} From d374d241f2a0b56bed07d8b02ea175c5e68fd186 Mon Sep 17 00:00:00 2001 From: Teppo Koskinen Date: Fri, 19 Mar 2021 12:26:05 +0200 Subject: [PATCH 089/516] Update and rename Library.py to MyLibrary.py --- templates/extended/libraries/{Library.py => MyLibrary.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename templates/extended/libraries/{Library.py => MyLibrary.py} (90%) diff --git a/templates/extended/libraries/Library.py b/templates/extended/libraries/MyLibrary.py similarity index 90% rename from templates/extended/libraries/Library.py rename to templates/extended/libraries/MyLibrary.py index e319ecbf..5f993eea 100644 --- a/templates/extended/libraries/Library.py +++ b/templates/extended/libraries/MyLibrary.py @@ -1,7 +1,7 @@ from robot.api import logger -class Library: +class MyLibrary: """Give this library a proper name and document it.""" def example_python_keyword(self): From 7165ee08b407cc45da11641ad4b39c980252667a Mon Sep 17 00:00:00 2001 From: Teppo Koskinen Date: Fri, 19 Mar 2021 12:27:33 +0200 Subject: [PATCH 090/516] python import to use updated module name --- templates/extended/tasks.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/extended/tasks.robot b/templates/extended/tasks.robot index 5eb52e57..61112764 100644 --- a/templates/extended/tasks.robot +++ b/templates/extended/tasks.robot @@ -1,7 +1,7 @@ *** Settings *** Documentation Template robot main suite. Resource keywords.robot -Library Library.py +Library MyLibrary Variables variables.py From 2614ec71a74c7b339a22d60fbb28745a30da7812 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 22 Mar 2021 13:25:36 +0200 Subject: [PATCH 091/516] RCC-158: settings.yaml (v9.8.1) - ALPHA level pre-release (do not use, unless you know what you are doing) - now some parts of settings are used from settings.yaml - settings.yaml is now critical part of rcc, so diagnostics also contains it - also from now, problems in settings.yaml may make rcc to fail - changed ephemeral key size to 2048, which should be good enough --- assets/settings.yaml | 2 +- cloud/metrics.go | 9 +++-- cmd/credentials.go | 2 +- common/commander.go | 20 ++++++++++ common/diagnostics.go | 8 ++++ common/variables.go | 12 ++++-- common/version.go | 2 +- conda/workflows.go | 16 +++----- docs/changelog.md | 8 ++++ operations/credentials.go | 2 +- operations/credentials_test.go | 2 +- operations/diagnostics.go | 7 ++-- operations/issues.go | 7 +++- operations/security.go | 2 +- operations/security_test.go | 8 ++-- operations/settings.go | 68 +++++++++++++++++++++++++--------- settings/data.go | 41 ++++++++++++++++++++ 17 files changed, 167 insertions(+), 49 deletions(-) create mode 100644 common/commander.go diff --git a/assets/settings.yaml b/assets/settings.yaml index 6d98ba5a..3d8e7544 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,5 +1,5 @@ endpoints: - cloud-api: https://api.eu1.robocorp.com + cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://id.robocorp.com pypi: https://pypi.org/simple/ conda: https://repo.anaconda.org diff --git a/cloud/metrics.go b/cloud/metrics.go index 7749d9d3..1fac190d 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -16,10 +16,9 @@ var ( const ( trackingUrl = `/metric-v1/%v/%v/%v/%v/%v` - metricsHost = `https://telemetry.robocorp.com` ) -func sendMetric(kind, name, value string) { +func sendMetric(metricsHost, kind, name, value string) { common.Timeline("%s:%s = %s", kind, name, value) defer func() { status := recover() @@ -40,10 +39,14 @@ func sendMetric(kind, name, value string) { } func BackgroundMetric(kind, name, value string) { + metricsHost := common.Settings.TelemetryURL() + if len(metricsHost) == 0 { + return + } common.Debug("DEBUG: BackgroundMetric kind:%v name:%v value:%v send:%v", kind, name, value, xviper.CanTrack()) if xviper.CanTrack() { telemetryBarrier.Add(1) - go sendMetric(kind, name, value) + go sendMetric(metricsHost, kind, name, value) } } diff --git a/cmd/credentials.go b/cmd/credentials.go index 57b42bb9..f77e7f51 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -45,7 +45,7 @@ var credentialsCmd = &cobra.Command{ } endpoint = endpointUrl if len(endpoint) == 0 { - endpoint = common.DefaultEndpoint + endpoint = common.Settings.DefaultEndpoint() } https, err := cloud.EnsureHttps(endpoint) if err != nil { diff --git a/common/commander.go b/common/commander.go new file mode 100644 index 00000000..561a3145 --- /dev/null +++ b/common/commander.go @@ -0,0 +1,20 @@ +package common + +type Commander struct { + command []string +} + +func (it *Commander) Option(name, value string) *Commander { + if len(value) > 0 { + it.command = append(it.command, name, value) + } + return it +} + +func (it *Commander) CLI() []string { + return it.command +} + +func NewCommander(parts ...string) *Commander { + return &Commander{parts} +} diff --git a/common/diagnostics.go b/common/diagnostics.go index 9d19ab82..1e88830d 100644 --- a/common/diagnostics.go +++ b/common/diagnostics.go @@ -52,6 +52,14 @@ func (it *DiagnosticStatus) Diagnose(kind string) Diagnoser { } } +func (it *DiagnosticStatus) Counts() (fatal, fail, warning, ok int) { + result := make(map[string]int) + for _, check := range it.Checks { + result[check.Status] += 1 + } + return result[StatusFatal], result[StatusFail], result[StatusWarning], result[StatusOk] +} + func (it *DiagnosticStatus) AsJson() (string, error) { body, err := json.MarshalIndent(it, "", " ") if err != nil { diff --git a/common/variables.go b/common/variables.go index 09a1dfa4..70a612ed 100644 --- a/common/variables.go +++ b/common/variables.go @@ -21,11 +21,17 @@ var ( SemanticTag string When int64 Clock *stopwatch + Settings SettingsHold ) -const ( - DefaultEndpoint = "https://api.eu1.robocloud.eu/" -) +type SettingsHold interface { + DefaultEndpoint() string + TelemetryURL() string + IssuesURL() string + PypiURL() string + CondaURL() string + DownloadsURL() string +} func init() { Clock = &stopwatch{"Clock", time.Now()} diff --git a/common/version.go b/common/version.go index 20a52120..0fc4c630 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.0` + Version = `v9.8.1` ) diff --git a/conda/workflows.go b/conda/workflows.go index 5857bbe6..b98d728c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -186,16 +186,14 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - command := []string{BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder} - if true || common.DebugFlag { - command = []string{BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder} - } + mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder) + //mambaCommand.Option("--channel", common.Settings.CondaURL()) observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") common.Timeline("Micromamba start.") fmt.Fprintf(planWriter, "\n--- micromamba plan @%ss ---\n\n", stopwatch) tee := io.MultiWriter(observer, planWriter) - code, err := shell.New(CondaEnvironment(), ".", command...).Tracked(tee, false) + code, err := shell.New(CondaEnvironment(), ".", mambaCommand.CLI()...).Tracked(tee, false) if err != nil || code != 0 { common.Timeline("micromamba fail.") common.Fatal("Micromamba", err) @@ -215,12 +213,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Log("#### Progress: 4/6 [pip install phase]") common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) - pipCommand := []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet"} - if true || common.DebugFlag { - pipCommand = []string{"pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText} - } + pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet") + pipCommand.Option("--index-url", common.Settings.PypiURL()) common.Debug("=== new live --- pip install phase ===") - err = LiveExecution(planWriter, targetFolder, pipCommand...) + err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil { common.Timeline("pip fail.") common.Fatal("Pip", err) diff --git a/docs/changelog.md b/docs/changelog.md index 0224bd54..30768145 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v9.8.1 (date: 22.3.2021) + +- ALPHA level pre-release (do not use, unless you know what you are doing) +- now some parts of settings are used from settings.yaml +- settings.yaml is now critical part of rcc, so diagnostics also contains it +- also from now, problems in settings.yaml may make rcc to fail +- changed ephemeral key size to 2048, which should be good enough + ## v9.8.0 (date: 18.3.2021) - ALPHA level pre-release with settings.yaml (do not use, unless you know diff --git a/operations/credentials.go b/operations/credentials.go index db721ae4..173f9e6c 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -216,7 +216,7 @@ func loadAccount(label string) *account { func createEphemeralAccount(parts []string) *account { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.account.ephemeral", common.Version) common.NoCache = true - endpoint := common.DefaultEndpoint + endpoint := common.Settings.DefaultEndpoint() if len(parts[3]) > 0 { endpoint = parts[3] } diff --git a/operations/credentials_test.go b/operations/credentials_test.go index 464e57d1..c0e32d98 100644 --- a/operations/credentials_test.go +++ b/operations/credentials_test.go @@ -20,7 +20,7 @@ func TestCanGetEphemeralDefaultEndpointAccountByName(t *testing.T) { wont_be.Nil(sut) must_be.Equal("Ephemeral", sut.Account) must_be.Equal("1111", sut.Identifier) - must_be.Equal(common.DefaultEndpoint, sut.Endpoint) + must_be.Equal(common.Settings.DefaultEndpoint(), sut.Endpoint) must_be.Equal("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", sut.Secret) wont_be.True(sut.Default) } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index c7b5a578..c7264b26 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -25,7 +25,6 @@ import ( ) const ( - canaryHost = `https://downloads.robocorp.com` canaryUrl = `/canary.txt` supportLongPathUrl = `https://robocorp.com/docs/troubleshooting/windows-long-path` supportNetworkUrl = `https://robocorp.com/docs/troubleshooting/firewall-and-proxies` @@ -155,12 +154,12 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { } func canaryDownloadCheck() *common.DiagnosticCheck { - client, err := cloud.NewClient(canaryHost) + client, err := cloud.NewClient(common.Settings.DownloadsURL()) if err != nil { return &common.DiagnosticCheck{ Type: "network", Status: statusFail, - Message: fmt.Sprintf("%v: %v", canaryHost, err), + Message: fmt.Sprintf("%v: %v", common.Settings.DownloadsURL(), err), Link: supportNetworkUrl, } } @@ -177,7 +176,7 @@ func canaryDownloadCheck() *common.DiagnosticCheck { return &common.DiagnosticCheck{ Type: "network", Status: statusOk, - Message: fmt.Sprintf("Canary download successful: %s%s", canaryHost, canaryUrl), + Message: fmt.Sprintf("Canary download successful: %s%s", common.Settings.DownloadsURL(), canaryUrl), Link: supportNetworkUrl, } } diff --git a/operations/issues.go b/operations/issues.go index 93bfb762..70bf720e 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -16,8 +16,7 @@ import ( ) const ( - issueHost = `https://telemetry.robocorp.com` - issueUrl = `/diagnostics-v1/issue` + issueUrl = `/diagnostics-v1/issue` ) func loadToken(reportFile string) (Token, error) { @@ -76,6 +75,10 @@ func virtualName(filename string) (string, error) { } func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, dryrun bool) error { + issueHost := common.Settings.IssuesURL() + if len(issueHost) == 0 { + return nil + } cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) token, err := loadToken(reportFile) if err != nil { diff --git a/operations/security.go b/operations/security.go index 4680dd53..9ce06109 100644 --- a/operations/security.go +++ b/operations/security.go @@ -36,7 +36,7 @@ func Decoded(content string) ([]byte, error) { } func GenerateEphemeralKey() (*EncryptionV1, error) { - key, err := rsa.GenerateKey(rand.Reader, 4096) + key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err } diff --git a/operations/security_test.go b/operations/security_test.go index 9cae8850..b2ad5054 100644 --- a/operations/security_test.go +++ b/operations/security_test.go @@ -18,12 +18,12 @@ func TestCanCreatePrivateKey(t *testing.T) { wont.Nil(key.Public()) publicKey, ok := key.Public().(*rsa.PublicKey) must.True(ok) - must.Equal(512, publicKey.Size()) - must.Equal(704, len(key.PublicDER())) - must.Equal(775, len(key.PublicPEM())) + must.Equal(256, publicKey.Size()) + must.Equal(360, len(key.PublicDER())) + must.Equal(426, len(key.PublicPEM())) body, err := key.RequestObject(nil) must.Nil(err) - must.Equal(847, len(body)) + must.Equal(493, len(body)) textual := string(body) must.True(strings.Contains(textual, "encryption")) must.True(strings.Contains(textual, "scheme")) diff --git a/operations/settings.go b/operations/settings.go index 10fe5181..893ec826 100644 --- a/operations/settings.go +++ b/operations/settings.go @@ -1,7 +1,6 @@ package operations import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -16,6 +15,7 @@ import ( var ( cachedSettings *settings.Settings + Settings gateway ) func cacheSettings(result *settings.Settings) (*settings.Settings, error) { @@ -63,27 +63,61 @@ func SummonSettings() (*settings.Settings, error) { return cacheSettings(config.Source(source)) } -func DefaultEndpoint() (string, error) { - config, err := SummonSettings() - if err != nil { - return "", err - } - endpoints := config.Endpoints - if endpoints == nil { - return "", fmt.Errorf("Brokens settings: all endpoints are missing!") - } - return "", nil -} - func CriticalEnvironmentSettingsCheck() { - return - // JIPPO:FIXME:JIPPO -- continue here config, err := SummonSettings() pretty.Guard(err == nil, 80, "Aborting! Could not even get setting, reason: %v", err) result := &common.DiagnosticStatus{ Details: make(map[string]string), Checks: []*common.DiagnosticCheck{}, } - config.Diagnostics(result) - humaneDiagnostics(os.Stderr, result) + config.CriticalEnvironmentDiagnostics(result) + diagnose := result.Diagnose("Settings") + if HasCustomSettings() { + diagnose.Ok("Uses custom settings at %q.", SettingsFileLocation()) + } else { + diagnose.Ok("Uses builtin settings.") + } + fatal, fail, _, _ := result.Counts() + if (fatal + fail) > 0 { + humaneDiagnostics(os.Stderr, result) + pretty.Guard(false, 111, "\nBroken settings.yaml. Cannot continue!") + } +} + +type gateway bool + +func (it gateway) Endpoints() *settings.Endpoints { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + pretty.Guard(config.Endpoints != nil, 111, "settings.yaml: endpoints are missing") + return config.Endpoints +} + +func (it gateway) DefaultEndpoint() string { + return it.Endpoints().CloudApi +} + +func (it gateway) IssuesURL() string { + return it.Endpoints().Issues +} + +func (it gateway) TelemetryURL() string { + return it.Endpoints().Telemetry +} + +func (it gateway) PypiURL() string { + return it.Endpoints().Pypi +} + +func (it gateway) CondaURL() string { + return it.Endpoints().Conda +} + +func (it gateway) DownloadsURL() string { + return it.Endpoints().Downloads +} + +func init() { + Settings = gateway(true) + common.Settings = Settings } diff --git a/settings/data.go b/settings/data.go index aee8795e..14b84abd 100644 --- a/settings/data.go +++ b/settings/data.go @@ -2,11 +2,17 @@ package settings import ( "encoding/json" + "net/url" + "strings" "github.com/robocorp/rcc/common" "gopkg.in/yaml.v1" ) +const ( + httpsPrefix = `https://` +) + type StringMap map[string]string type Settings struct { @@ -52,6 +58,40 @@ func (it *Settings) AsJson() ([]byte, error) { return content, nil } +func diagnoseUrl(link, label string, diagnose common.Diagnoser, correct bool) bool { + if len(link) == 0 { + diagnose.Fatal("", "required %q URL is missing.", label) + return false + } + if !strings.HasPrefix(link, httpsPrefix) { + diagnose.Fatal("", "%q URL %q is does not start with %q prefix.", label, link, httpsPrefix) + return false + } + _, err := url.Parse(link) + if err != nil { + diagnose.Fatal("", "%q URL %q cannot be parsed, reason %v.", label, link, err) + return false + } + return correct +} + +func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStatus) { + diagnose := target.Diagnose("settings.yaml") + correct := true + if it.Endpoints == nil { + diagnose.Fatal("", "endpoints section is totally missing") + correct = false + } else { + correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) + } + if correct { + diagnose.Ok("Toplevel settings are ok.") + } +} + func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { diagnose := target.Diagnose("Settings") correct := true @@ -64,6 +104,7 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { correct = false } if it.Endpoints == nil { + diagnose.Warning("", "settings.yaml: endpoints section is totally missing") correct = false } if it.Logs == nil { From 872d781b5693c402f6e4e65db41a36d816c8a388 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 23 Mar 2021 10:36:13 +0200 Subject: [PATCH 092/516] RCC-158: settings.yaml (v9.8.2) - ALPHA level pre-release (do not use, unless you know what you are doing) - reorganizing some code to allow better use of settings.yaml - more values from settings.yaml are now used --- assets/settings.yaml | 6 ++- cloud/metrics.go | 3 +- cmd/credentials.go | 3 +- cmd/env.go | 4 +- cmd/root.go | 2 +- cmd/settings.go | 6 +-- common/platform_darwin.go | 19 ++++++++ common/platform_linux.go | 19 ++++++++ common/platform_windows.go | 37 +++++++++++++++ common/variables.go | 53 +++++++++++++++++---- common/version.go | 2 +- conda/activate.go | 2 +- conda/cleanup.go | 12 ++--- conda/platform_darwin_amd64.go | 29 ++++-------- conda/platform_linux_amd64.go | 31 ++++-------- conda/platform_test.go | 6 +-- conda/platform_windows_amd64.go | 56 ++++++---------------- conda/robocorp.go | 71 ++++++---------------------- conda/validate.go | 2 +- conda/workflows.go | 11 +++-- docs/changelog.md | 6 +++ operations/cache.go | 4 +- operations/credentials.go | 3 +- operations/credentials_test.go | 4 +- operations/diagnostics.go | 30 ++++-------- operations/issues.go | 5 +- operations/robotcache.go | 5 +- robot/robot.go | 2 +- settings/data.go | 41 +++++++++++++++- {operations => settings}/settings.go | 36 +++++++++----- settings/settings_test.go | 19 ++++++++ 31 files changed, 307 insertions(+), 222 deletions(-) create mode 100644 common/platform_darwin.go create mode 100644 common/platform_linux.go create mode 100644 common/platform_windows.go rename {operations => settings}/settings.go (76%) create mode 100644 settings/settings_test.go diff --git a/assets/settings.yaml b/assets/settings.yaml index 3d8e7544..ed78da4f 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -2,6 +2,8 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://id.robocorp.com pypi: https://pypi.org/simple/ + pypi-files: https://files.pythonhosted.org + pypi-trusted: conda: https://repo.anaconda.org downloads: https://downloads.robocorp.com docs: https://robocorp.com/docs/ @@ -20,11 +22,11 @@ autoupdates: lab: https://downloads.code.robocorp.com/lab/installer certificates: - ignore-ssl-verification: false + verify-ssl: true root-location: logs: - level: debug + level: normal root-location: "%localappdata%\\robocorp\\logs" business-data: diff --git a/cloud/metrics.go b/cloud/metrics.go index 1fac190d..2b693aaf 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -7,6 +7,7 @@ import ( "time" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -39,7 +40,7 @@ func sendMetric(metricsHost, kind, name, value string) { } func BackgroundMetric(kind, name, value string) { - metricsHost := common.Settings.TelemetryURL() + metricsHost := settings.Global.TelemetryURL() if len(metricsHost) == 0 { return } diff --git a/cmd/credentials.go b/cmd/credentials.go index f77e7f51..50a29eb1 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -7,6 +7,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -45,7 +46,7 @@ var credentialsCmd = &cobra.Command{ } endpoint = endpointUrl if len(endpoint) == 0 { - endpoint = common.Settings.DefaultEndpoint() + endpoint = settings.Global.DefaultEndpoint() } https, err := cloud.EnsureHttps(endpoint) if err != nil { diff --git a/cmd/env.go b/cmd/env.go index dd4d0816..3b77f151 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -2,7 +2,7 @@ package cmd import ( "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -13,7 +13,7 @@ var envCmd = &cobra.Command{ Long: `This "env" command set is for managing virtual environments, used in task context locally.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { - operations.CriticalEnvironmentSettingsCheck() + settings.CriticalEnvironmentSettingsCheck() }, } diff --git a/cmd/root.go b/cmd/root.go index 439ddceb..eb0a52e7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -95,7 +95,7 @@ func initConfig() { if cfgFile != "" { xviper.SetConfigFile(cfgFile) } else { - xviper.SetConfigFile(filepath.Join(conda.RobocorpHome(), "rcc.yaml")) + xviper.SetConfigFile(filepath.Join(common.RobocorpHome(), "rcc.yaml")) } common.UnifyVerbosityFlags() diff --git a/cmd/settings.go b/cmd/settings.go index 75251bc0..d3414f6e 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -15,13 +15,13 @@ var settingsCmd = &cobra.Command{ Long: "Show default settings.yaml content.", Run: func(cmd *cobra.Command, args []string) { if jsonFlag { - config, err := operations.SummonSettings() + config, err := settings.SummonSettings() pretty.Guard(err == nil, 2, "Error while loading settings: %v", err) json, err := config.AsJson() pretty.Guard(err == nil, 3, "Error while converting settings: %v", err) fmt.Fprintf(os.Stdout, "%s", string(json)) } else { - raw, err := operations.DefaultSettings() + raw, err := settings.DefaultSettings() pretty.Guard(err == nil, 1, "Error while loading defaults: %v", err) fmt.Fprintf(os.Stdout, "%s", string(raw)) } diff --git a/common/platform_darwin.go b/common/platform_darwin.go new file mode 100644 index 00000000..1678ffa4 --- /dev/null +++ b/common/platform_darwin.go @@ -0,0 +1,19 @@ +package common + +import ( + "os" + "path/filepath" +) + +const ( + defaultRobocorpLocation = "$HOME/.robocorp" +) + +func ExpandPath(entry string) string { + intermediate := os.ExpandEnv(entry) + result, err := filepath.Abs(intermediate) + if err != nil { + return intermediate + } + return result +} diff --git a/common/platform_linux.go b/common/platform_linux.go new file mode 100644 index 00000000..1678ffa4 --- /dev/null +++ b/common/platform_linux.go @@ -0,0 +1,19 @@ +package common + +import ( + "os" + "path/filepath" +) + +const ( + defaultRobocorpLocation = "$HOME/.robocorp" +) + +func ExpandPath(entry string) string { + intermediate := os.ExpandEnv(entry) + result, err := filepath.Abs(intermediate) + if err != nil { + return intermediate + } + return result +} diff --git a/common/platform_windows.go b/common/platform_windows.go new file mode 100644 index 00000000..49bd24b2 --- /dev/null +++ b/common/platform_windows.go @@ -0,0 +1,37 @@ +package common + +import ( + "os" + "path/filepath" + "regexp" +) + +const ( + defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" +) + +var ( + variablePattern = regexp.MustCompile("%[a-zA-Z]+%") +) + +func ExpandPath(entry string) string { + intermediate := os.ExpandEnv(entry) + intermediate = variablePattern.ReplaceAllStringFunc(intermediate, fromEnvironment) + result, err := filepath.Abs(intermediate) + if err != nil { + return intermediate + } + return result +} + +func fromEnvironment(form string) string { + replacement, ok := os.LookupEnv(form[1 : len(form)-1]) + if ok { + return replacement + } + replacement, ok = os.LookupEnv(form) + if ok { + return replacement + } + return form +} diff --git a/common/variables.go b/common/variables.go index 70a612ed..d64d027a 100644 --- a/common/variables.go +++ b/common/variables.go @@ -2,10 +2,16 @@ package common import ( "fmt" + "os" + "path/filepath" "strings" "time" ) +const ( + ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` +) + var ( Silent bool DebugFlag bool @@ -21,23 +27,50 @@ var ( SemanticTag string When int64 Clock *stopwatch - Settings SettingsHold ) -type SettingsHold interface { - DefaultEndpoint() string - TelemetryURL() string - IssuesURL() string - PypiURL() string - CondaURL() string - DownloadsURL() string -} - func init() { Clock = &stopwatch{"Clock", time.Now()} When = Clock.started.Unix() } +func RobocorpHome() string { + home := os.Getenv(ROBOCORP_HOME_VARIABLE) + if len(home) > 0 { + return ExpandPath(home) + } + return ExpandPath(defaultRobocorpLocation) +} + +func ensureDirectory(name string) string { + os.MkdirAll(name, 0o750) + return name +} + +func BinLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "bin")) +} + +func LiveLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "live")) +} + +func TemplateLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "base")) +} + +func PipCache() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "pipcache")) +} + +func WheelCache() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "wheels")) +} + +func RobotCache() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "robots")) +} + func UnifyVerbosityFlags() { if Silent { DebugFlag = false diff --git a/common/version.go b/common/version.go index 0fc4c630..317a3a0f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.1` + Version = `v9.8.2` ) diff --git a/conda/activate.go b/conda/activate.go index b46c7f68..1a74a535 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -62,7 +62,7 @@ func createScript(targetFolder string) (string, error) { } details := make(map[string]string) details["Rcc"] = BinRcc() - details["Robocorphome"] = RobocorpHome() + details["Robocorphome"] = common.RobocorpHome() details["Micromamba"] = BinMicromamba() details["Live"] = targetFolder buffer := bytes.NewBuffer(nil) diff --git a/conda/cleanup.go b/conda/cleanup.go index 09b7a56d..0c0cc527 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -64,17 +64,17 @@ func spotlessCleanup(dryrun bool) error { } if dryrun { common.Log("Would be removing:") - common.Log("- %v", TemplateLocation()) - common.Log("- %v", LiveLocation()) - common.Log("- %v", PipCache()) + common.Log("- %v", common.TemplateLocation()) + common.Log("- %v", common.LiveLocation()) + common.Log("- %v", common.PipCache()) common.Log("- %v", MambaPackages()) common.Log("- %v", BinMicromamba()) common.Log("- %v", RobocorpTempRoot()) return nil } - safeRemove("cache", TemplateLocation()) - safeRemove("cache", LiveLocation()) - safeRemove("cache", PipCache()) + safeRemove("cache", common.TemplateLocation()) + safeRemove("cache", common.LiveLocation()) + safeRemove("cache", common.PipCache()) safeRemove("cache", MambaPackages()) safeRemove("temp", RobocorpTempRoot()) safeRemove("executable", BinMicromamba()) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index e5df42d0..f1e6158c 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -4,13 +4,15 @@ import ( "fmt" "os" "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" ) const ( - Newline = "\n" - defaultRobocorpLocation = "$HOME/.robocorp" - binSuffix = "/bin" - activateScript = `#!/bin/bash + Newline = "\n" + binSuffix = "/bin" + activateScript = `#!/bin/bash export MAMBA_ROOT_PREFIX={{.Robocorphome}} eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" @@ -24,18 +26,9 @@ var ( FileExtensions = []string{"", ".sh"} ) -func ExpandPath(entry string) string { - intermediate := os.ExpandEnv(entry) - result, err := filepath.Abs(intermediate) - if err != nil { - return intermediate - } - return result -} - func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) tempFolder := RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) @@ -43,7 +36,7 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return ExpandPath(filepath.Join(BinLocation(), "micromamba")) + return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba")) } func CondaPaths(prefix string) []string { @@ -51,11 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.2/macos64/micromamba" -} - -func IsPosix() bool { - return true + return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/macos64/micromamba" } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 16e02cec..c177c72d 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -4,13 +4,15 @@ import ( "fmt" "os" "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" ) const ( - Newline = "\n" - defaultRobocorpLocation = "$HOME/.robocorp" - binSuffix = "/bin" - activateScript = `#!/bin/bash + Newline = "\n" + binSuffix = "/bin" + activateScript = `#!/bin/bash export MAMBA_ROOT_PREFIX={{.Robocorphome}} eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" @@ -25,21 +27,12 @@ var ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.2/linux64/micromamba" -} - -func ExpandPath(entry string) string { - intermediate := os.ExpandEnv(entry) - result, err := filepath.Abs(intermediate) - if err != nil { - return intermediate - } - return result + return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/linux64/micromamba" } func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) tempFolder := RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) @@ -47,15 +40,11 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return ExpandPath(filepath.Join(BinLocation(), "micromamba")) + return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba")) } func CondaPaths(prefix string) []string { - return []string{ExpandPath(prefix + binSuffix)} -} - -func IsPosix() bool { - return true + return []string{common.ExpandPath(prefix + binSuffix)} } func IsWindows() bool { diff --git a/conda/platform_test.go b/conda/platform_test.go index 59efdead..2dcbb343 100644 --- a/conda/platform_test.go +++ b/conda/platform_test.go @@ -3,6 +3,7 @@ package conda_test import ( "testing" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/hamlet" ) @@ -13,7 +14,7 @@ func TestExpandingPath(t *testing.T) { } _, wont_be := hamlet.Specifications(t) - wont_be.Equal("$HOME/bin", conda.ExpandPath("$HOME/bin")) + wont_be.Equal("$HOME/bin", common.ExpandPath("$HOME/bin")) } func TestCondaPathSetup(t *testing.T) { @@ -31,8 +32,7 @@ func TestFlagsAreCorrectlySet(t *testing.T) { if conda.IsWindows() { t.Skip("Not a windows test.") } - must_be, wont_be := hamlet.Specifications(t) + _, wont_be := hamlet.Specifications(t) wont_be.True(conda.IsWindows()) - must_be.True(conda.IsPosix()) } diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index a7a0b19f..b22ff767 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -4,24 +4,23 @@ import ( "fmt" "os" "path/filepath" - "regexp" "golang.org/x/sys/windows/registry" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" ) const ( - mingwSuffix = "\\mingw-w64" - Newline = "\r\n" - defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" - librarySuffix = "\\Library" - scriptSuffix = "\\Scripts" - usrSuffix = "\\usr" - binSuffix = "\\bin" - activateScript = "@echo off\n" + + mingwSuffix = "\\mingw-w64" + Newline = "\r\n" + librarySuffix = "\\Library" + scriptSuffix = "\\Scripts" + usrSuffix = "\\usr" + binSuffix = "\\bin" + activateScript = "@echo off\n" + "set MAMBA_ROOT_PREFIX=\"{{.Robocorphome}}\"\n" + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell -s cmd.exe activate -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + "call \"{{.Rcc}}\" internal env -l after\n" @@ -29,44 +28,21 @@ const ( ) func MicromambaLink() string { - return "https://downloads.robocorp.com/micromamba/v0.8.2/windows64/micromamba.exe" + return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/windows64/micromamba.exe" } var ( - Shell = []string{"cmd.exe", "/K"} - variablePattern = regexp.MustCompile("%[a-zA-Z]+%") - FileExtensions = []string{".exe", ".com", ".bat", ".cmd", ""} + Shell = []string{"cmd.exe", "/K"} + FileExtensions = []string{".exe", ".com", ".bat", ".cmd", ""} ) -func fromEnvironment(form string) string { - replacement, ok := os.LookupEnv(form[1 : len(form)-1]) - if ok { - return replacement - } - replacement, ok = os.LookupEnv(form) - if ok { - return replacement - } - return form -} - -func ExpandPath(entry string) string { - intermediate := os.ExpandEnv(entry) - intermediate = variablePattern.ReplaceAllStringFunc(intermediate, fromEnvironment) - result, err := filepath.Abs(intermediate) - if err != nil { - return intermediate - } - return result -} - func ensureHardlinkEnvironmment() (string, error) { return "", fmt.Errorf("Not implemented yet!") } func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) tempFolder := RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) @@ -74,7 +50,7 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return ExpandPath(filepath.Join(BinLocation(), "micromamba.exe")) + return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba.exe")) } func CondaPaths(prefix string) []string { @@ -88,16 +64,12 @@ func CondaPaths(prefix string) []string { } } -func IsPosix() bool { - return false -} - func IsWindows() bool { return true } func HasLongPathSupport() bool { - baseline := []string{RobocorpHome(), "stump"} + baseline := []string{common.RobocorpHome(), "stump"} stumpath := filepath.Join(baseline...) defer os.RemoveAll(stumpath) diff --git a/conda/robocorp.go b/conda/robocorp.go index 04b665cf..3ed34405 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -17,10 +17,6 @@ import ( "github.com/robocorp/rcc/xviper" ) -const ( - ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` -) - var ( ignoredPaths = []string{"python", "conda"} pythonPaths = []string{"resources", "libraries", "tasks", "variables"} @@ -100,7 +96,7 @@ func hasMetafile(basedir, subdir string) bool { func dirnamesFrom(location string) []string { result := make([]string, 0, 20) - handle, err := os.Open(ExpandPath(location)) + handle, err := os.Open(common.ExpandPath(location)) if err != nil { common.Error("Warning", err) return result @@ -123,7 +119,7 @@ func dirnamesFrom(location string) []string { func orphansFrom(location string) []string { result := make([]string, 0, 20) - handle, err := os.Open(ExpandPath(location)) + handle, err := os.Open(common.ExpandPath(location)) if err != nil { common.Error("Warning", err) return result @@ -179,7 +175,7 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", - "ROBOCORP_HOME="+RobocorpHome(), + "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), @@ -197,11 +193,11 @@ func EnvironmentFor(location string) []string { } func MambaPackages() string { - return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) + return common.ExpandPath(filepath.Join(common.RobocorpHome(), "pkgs")) } func MambaCache() string { - return ExpandPath(filepath.Join(MambaPackages(), "cache")) + return common.ExpandPath(filepath.Join(MambaPackages(), "cache")) } func asVersion(text string) (uint64, string) { @@ -246,16 +242,8 @@ func HasMicroMamba() bool { return goodEnough } -func RobocorpHome() string { - home := os.Getenv(ROBOCORP_HOME_VARIABLE) - if len(home) > 0 { - return ExpandPath(home) - } - return ExpandPath(defaultRobocorpLocation) -} - func RobocorpTempRoot() string { - return filepath.Join(RobocorpHome(), "temp") + return filepath.Join(common.RobocorpHome(), "temp") } func RobocorpTemp() string { @@ -267,46 +255,17 @@ func RobocorpTemp() string { return fullpath } -func BinLocation() string { - return filepath.Join(RobocorpHome(), "bin") -} - -func LiveLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "live")) -} - -func TemplateLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "base")) -} - func RobocorpLock() string { - return fmt.Sprintf("%s.lck", LiveLocation()) + return fmt.Sprintf("%s.lck", common.LiveLocation()) } func MinicondaLocation() string { // Legacy function, but must remain until cleanup is done - return filepath.Join(RobocorpHome(), "miniconda3") -} - -func ensureDirectory(name string) string { - pathlib.EnsureDirectoryExists(name) - return name -} - -func PipCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "pipcache")) -} - -func WheelCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "wheels")) -} - -func RobotCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "robots")) + return filepath.Join(common.RobocorpHome(), "miniconda3") } func LocalChannel() (string, bool) { - basefolder := filepath.Join(RobocorpHome(), "channel") + basefolder := filepath.Join(common.RobocorpHome(), "channel") fullpath := filepath.Join(basefolder, "channeldata.json") stats, err := os.Stat(fullpath) if err != nil { @@ -319,27 +278,27 @@ func LocalChannel() (string, bool) { } func TemplateFrom(hash string) string { - return filepath.Join(TemplateLocation(), hash) + return filepath.Join(common.TemplateLocation(), hash) } func LiveFrom(hash string) string { if common.Stageonly { return common.StageFolder } - return ExpandPath(filepath.Join(LiveLocation(), hash)) + return common.ExpandPath(filepath.Join(common.LiveLocation(), hash)) } func TemplateList() []string { - return dirnamesFrom(TemplateLocation()) + return dirnamesFrom(common.TemplateLocation()) } func LiveList() []string { - return dirnamesFrom(LiveLocation()) + return dirnamesFrom(common.LiveLocation()) } func OrphanList() []string { - result := orphansFrom(TemplateLocation()) - result = append(result, orphansFrom(LiveLocation())...) + result := orphansFrom(common.TemplateLocation()) + result = append(result, orphansFrom(common.LiveLocation())...) return result } diff --git a/conda/validate.go b/conda/validate.go index 0bb18628..f75f577d 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -40,7 +40,7 @@ func ValidateLocations() bool { checked := map[string]string{ //"Environment variable 'TMP'": os.Getenv("TMP"), //"Environment variable 'TEMP'": os.Getenv("TEMP"), - "Path to 'ROBOCORP_HOME' directory": RobocorpHome(), + "Path to 'ROBOCORP_HOME' directory": common.RobocorpHome(), } // 7.1.2021 -- just warnings for now -- JMP:FIXME:JMP later validateLocations(checked) diff --git a/conda/workflows.go b/conda/workflows.go index b98d728c..831e73c5 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -17,6 +17,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" "github.com/robocorp/rcc/xviper" ) @@ -26,7 +27,7 @@ func Hexdigest(raw []byte) string { } func metafile(folder string) string { - return ExpandPath(folder + ".meta") + return common.ExpandPath(folder + ".meta") } func metaLoad(location string) (string, error) { @@ -132,7 +133,7 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) removeClone(targetFolder) - location := filepath.Join(RobocorpHome(), "pkgs") + location := filepath.Join(common.RobocorpHome(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) common.Log("%sWARNING! To fix it, try to remove directory: %v%s", pretty.Red, location, pretty.Reset) return true @@ -187,7 +188,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh ttl = "0" } mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder) - //mambaCommand.Option("--channel", common.Settings.CondaURL()) + //mambaCommand.Option("--channel", settings.Global.CondaURL()) observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") common.Timeline("Micromamba start.") @@ -204,7 +205,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, true } fmt.Fprintf(planWriter, "\n--- pip plan @%ss ---\n\n", stopwatch) - pipCache, wheelCache := PipCache(), WheelCache() + pipCache, wheelCache := common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { common.Log("#### Progress: 4/6 [pip install phase skipped -- no pip dependencies]") @@ -214,7 +215,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet") - pipCommand.Option("--index-url", common.Settings.PypiURL()) + pipCommand.Option("--index-url", settings.Global.PypiURL()) common.Debug("=== new live --- pip install phase ===") err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index 30768145..87ff68ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.8.2 (date: 23.3.2021) + +- ALPHA level pre-release (do not use, unless you know what you are doing) +- reorganizing some code to allow better use of settings.yaml +- more values from settings.yaml are now used + ## v9.8.1 (date: 22.3.2021) - ALPHA level pre-release (do not use, unless you know what you are doing) diff --git a/operations/cache.go b/operations/cache.go index 6ae8993f..30b6dc43 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/xviper" @@ -53,7 +53,7 @@ func cacheLocation() string { if len(reference) > 0 { return filepath.Join(filepath.Dir(reference), "rcccache.yaml") } else { - return filepath.Join(conda.RobocorpHome(), "rcccache.yaml") + return filepath.Join(common.RobocorpHome(), "rcccache.yaml") } } diff --git a/operations/credentials.go b/operations/credentials.go index 173f9e6c..0a4f8f29 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -216,7 +217,7 @@ func loadAccount(label string) *account { func createEphemeralAccount(parts []string) *account { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.account.ephemeral", common.Version) common.NoCache = true - endpoint := common.Settings.DefaultEndpoint() + endpoint := settings.Global.DefaultEndpoint() if len(parts[3]) > 0 { endpoint = parts[3] } diff --git a/operations/credentials_test.go b/operations/credentials_test.go index c0e32d98..8dd474e8 100644 --- a/operations/credentials_test.go +++ b/operations/credentials_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/hamlet" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -20,7 +20,7 @@ func TestCanGetEphemeralDefaultEndpointAccountByName(t *testing.T) { wont_be.Nil(sut) must_be.Equal("Ephemeral", sut.Account) must_be.Equal("1111", sut.Identifier) - must_be.Equal(common.Settings.DefaultEndpoint(), sut.Endpoint) + must_be.Equal(settings.Global.DefaultEndpoint(), sut.Endpoint) must_be.Equal("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", sut.Secret) wont_be.True(sut.Default) } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index c7264b26..8cb32670 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -20,6 +20,7 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" "gopkg.in/yaml.v1" ) @@ -35,17 +36,6 @@ const ( statusFatal = `fatal` ) -var ( - checkedHosts = []string{ - `api.eu1.robocloud.eu`, - `downloads.robocorp.com`, - `pypi.org`, - `conda.anaconda.org`, - `github.com`, - `files.pythonhosted.org`, - } -) - type stringerr func() (string, error) func justText(source stringerr) string { @@ -63,7 +53,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["rcc"] = common.Version result.Details["stats"] = rccStatusLine() result.Details["micromamba"] = conda.MicromambaVersion() - result.Details["ROBOCORP_HOME"] = conda.RobocorpHome() + result.Details["ROBOCORP_HOME"] = common.RobocorpHome() result.Details["user-cache-dir"] = justText(os.UserCacheDir) result.Details["user-config-dir"] = justText(os.UserConfigDir) result.Details["user-home-dir"] = justText(os.UserHomeDir) @@ -83,7 +73,7 @@ func RunDiagnostics() *common.DiagnosticStatus { // checks result.Checks = append(result.Checks, robocorpHomeCheck()) result.Checks = append(result.Checks, longPathSupportCheck()) - for _, host := range checkedHosts { + for _, host := range settings.Global.Hostnames() { result.Checks = append(result.Checks, dnsLookupCheck(host)) } result.Checks = append(result.Checks, canaryDownloadCheck()) @@ -119,18 +109,18 @@ func longPathSupportCheck() *common.DiagnosticCheck { } func robocorpHomeCheck() *common.DiagnosticCheck { - if !conda.ValidLocation(conda.RobocorpHome()) { + if !conda.ValidLocation(common.RobocorpHome()) { return &common.DiagnosticCheck{ Type: "RPA", Status: statusFatal, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", conda.RobocorpHome()), + Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", common.RobocorpHome()), Link: supportGeneralUrl, } } return &common.DiagnosticCheck{ Type: "RPA", Status: statusOk, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", conda.RobocorpHome()), + Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", common.RobocorpHome()), Link: supportGeneralUrl, } } @@ -141,7 +131,7 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { return &common.DiagnosticCheck{ Type: "network", Status: statusFail, - Message: fmt.Sprintf("DNS lookup %s failed: %v", site, err), + Message: fmt.Sprintf("DNS lookup %q failed: %v", site, err), Link: supportNetworkUrl, } } @@ -154,12 +144,12 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { } func canaryDownloadCheck() *common.DiagnosticCheck { - client, err := cloud.NewClient(common.Settings.DownloadsURL()) + client, err := cloud.NewClient(settings.Global.DownloadsURL()) if err != nil { return &common.DiagnosticCheck{ Type: "network", Status: statusFail, - Message: fmt.Sprintf("%v: %v", common.Settings.DownloadsURL(), err), + Message: fmt.Sprintf("%v: %v", settings.Global.DownloadsURL(), err), Link: supportNetworkUrl, } } @@ -176,7 +166,7 @@ func canaryDownloadCheck() *common.DiagnosticCheck { return &common.DiagnosticCheck{ Type: "network", Status: statusOk, - Message: fmt.Sprintf("Canary download successful: %s%s", common.Settings.DownloadsURL(), canaryUrl), + Message: fmt.Sprintf("Canary download successful: %s%s", settings.Global.DownloadsURL(), canaryUrl), Link: supportNetworkUrl, } } diff --git a/operations/issues.go b/operations/issues.go index 70bf720e..1b3a8277 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -44,7 +45,7 @@ func createIssueZip(attachmentsFiles []string) (string, error) { zipper.Add(attachment, niceName, nil) } // getting settings.yaml is optional, it should not break issue reporting - config, err := SummonSettings() + config, err := settings.SummonSettings() if err != nil { return zipfile, nil } @@ -75,7 +76,7 @@ func virtualName(filename string) (string, error) { } func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, dryrun bool) error { - issueHost := common.Settings.IssuesURL() + issueHost := settings.Global.IssuesURL() if len(issueHost) == 0 { return nil } diff --git a/operations/robotcache.go b/operations/robotcache.go index 13d00d41..f16874a3 100644 --- a/operations/robotcache.go +++ b/operations/robotcache.go @@ -8,7 +8,6 @@ import ( "time" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" ) @@ -50,7 +49,7 @@ func CacheRobot(filename string) error { } func cacheRobotFilename(digest string) string { - return filepath.Join(conda.RobotCache(), digest+".zip") + return filepath.Join(common.RobotCache(), digest+".zip") } func LookupRobot(digest string) (string, bool) { @@ -73,7 +72,7 @@ func CleanupOldestRobot() { func OldestRobot() (string, time.Time) { oldest, stamp := "", time.Now() deadline := time.Now().Add(-35 * 24 * time.Hour) - pathlib.Walk(conda.RobotCache(), pathlib.IgnoreNewer(deadline).Ignore, func(full, relative string, details os.FileInfo) { + pathlib.Walk(common.RobotCache(), pathlib.IgnoreNewer(deadline).Ignore, func(full, relative string, details os.FileInfo) { if zipPattern.MatchString(details.Name()) && details.ModTime().Before(stamp) { oldest, stamp = full, details.ModTime() } diff --git a/robot/robot.go b/robot/robot.go index cbc9c20b..dc499125 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -341,7 +341,7 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", - "ROBOCORP_HOME="+conda.RobocorpHome(), + "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), diff --git a/settings/data.go b/settings/data.go index 14b84abd..333883d2 100644 --- a/settings/data.go +++ b/settings/data.go @@ -3,6 +3,7 @@ package settings import ( "encoding/json" "net/url" + "sort" "strings" "github.com/robocorp/rcc/common" @@ -129,8 +130,8 @@ type BusinessData struct { } type Certificates struct { - IgnoreVerification bool `yaml:"ignore-ssl-verification" json:"ignore-ssl-verification"` - RootLocation string `yaml:"root-location" json:"root-location"` + VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` + RootLocation string `yaml:"root-location" json:"root-location"` } type Endpoints struct { @@ -142,10 +143,46 @@ type Endpoints struct { Issues string `yaml:"issues" json:"issues"` Portal string `yaml:"portal" json:"portal"` Pypi string `yaml:"pypi" json:"pypi"` + PypiFiles string `yaml:"pypi-files" json:"pypi-files"` + PypiTrusted string `yaml:"pypi-trusted" json:"pypi-trusted"` RobotPull string `yaml:"robot-pull" json:"robot-pull"` Telemetry string `yaml:"telemetry" json:"telemetry"` } +func hostFromUrl(link string, collector map[string]bool) { + if len(link) == 0 { + return + } + parsed, err := url.Parse(link) + if err != nil { + return + } + parts := strings.SplitN(parsed.Host, ":", 2) + collector[parts[0]] = true +} + +func (it *Endpoints) Hosts() []string { + collector := make(map[string]bool) + hostFromUrl(it.CloudApi, collector) + hostFromUrl(it.CloudLinking, collector) + hostFromUrl(it.Conda, collector) + hostFromUrl(it.Docs, collector) + hostFromUrl(it.Downloads, collector) + hostFromUrl(it.Issues, collector) + hostFromUrl(it.Portal, collector) + hostFromUrl(it.Pypi, collector) + hostFromUrl(it.PypiFiles, collector) + hostFromUrl(it.PypiTrusted, collector) + hostFromUrl(it.RobotPull, collector) + hostFromUrl(it.Telemetry, collector) + result := make([]string, 0, len(collector)) + for key, _ := range collector { + result = append(result, key) + } + sort.Strings(result) + return result +} + type Logs struct { Level string `yaml:"level" json:"level"` RootLocation string `yaml:"root-location" json:"root-location"` diff --git a/operations/settings.go b/settings/settings.go similarity index 76% rename from operations/settings.go rename to settings/settings.go index 893ec826..af2b9479 100644 --- a/operations/settings.go +++ b/settings/settings.go @@ -1,24 +1,24 @@ -package operations +package settings import ( + "fmt" + "io" "io/ioutil" "os" "path/filepath" "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/settings" ) var ( - cachedSettings *settings.Settings - Settings gateway + cachedSettings *Settings + Global gateway ) -func cacheSettings(result *settings.Settings) (*settings.Settings, error) { +func cacheSettings(result *Settings) (*Settings, error) { if result != nil { cachedSettings = result } @@ -26,7 +26,7 @@ func cacheSettings(result *settings.Settings) (*settings.Settings, error) { } func SettingsFileLocation() string { - return filepath.Join(conda.RobocorpHome(), "settings.yaml") + return filepath.Join(common.RobocorpHome(), "settings.yaml") } func HasCustomSettings() bool { @@ -48,7 +48,7 @@ func rawSettings() (content []byte, location string, err error) { } } -func SummonSettings() (*settings.Settings, error) { +func SummonSettings() (*Settings, error) { if cachedSettings != nil { return cachedSettings, nil } @@ -56,13 +56,20 @@ func SummonSettings() (*settings.Settings, error) { if err != nil { return nil, err } - config, err := settings.FromBytes(content) + config, err := FromBytes(content) if err != nil { return nil, err } return cacheSettings(config.Source(source)) } +func showDiagnosticsChecks(sink io.Writer, details *common.DiagnosticStatus) { + fmt.Fprintln(sink, "Checks:") + for _, check := range details.Checks { + fmt.Fprintf(sink, " - %-8s %-8s %s\n", check.Type, check.Status, check.Message) + } +} + func CriticalEnvironmentSettingsCheck() { config, err := SummonSettings() pretty.Guard(err == nil, 80, "Aborting! Could not even get setting, reason: %v", err) @@ -79,14 +86,14 @@ func CriticalEnvironmentSettingsCheck() { } fatal, fail, _, _ := result.Counts() if (fatal + fail) > 0 { - humaneDiagnostics(os.Stderr, result) + showDiagnosticsChecks(os.Stderr, result) pretty.Guard(false, 111, "\nBroken settings.yaml. Cannot continue!") } } type gateway bool -func (it gateway) Endpoints() *settings.Endpoints { +func (it gateway) Endpoints() *Endpoints { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) pretty.Guard(config.Endpoints != nil, 111, "settings.yaml: endpoints are missing") @@ -117,7 +124,10 @@ func (it gateway) DownloadsURL() string { return it.Endpoints().Downloads } +func (it gateway) Hostnames() []string { + return it.Endpoints().Hosts() +} + func init() { - Settings = gateway(true) - common.Settings = Settings + Global = gateway(true) } diff --git a/settings/settings_test.go b/settings/settings_test.go new file mode 100644 index 00000000..1932069f --- /dev/null +++ b/settings/settings_test.go @@ -0,0 +1,19 @@ +package settings_test + +import ( + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/settings" +) + +func TestCanCallEntropyFunction(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + sut, err := settings.SummonSettings() + must_be.Nil(err) + wont_be.Nil(sut) + + wont_be.Nil(settings.Global) + must_be.True(len(settings.Global.Hostnames()) > 1) +} From 84d951ce187af9c692c04a72f264e9dfa6d2e972 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Mar 2021 12:12:24 +0200 Subject: [PATCH 093/516] RCC-158: settings.yaml (v9.8.3) - can configure all rcc operations not to verify correct SSL certificate (please note, doing this is insecure and allows man-in-the-middle attacks) - applied reviewed changes to what is actually in settings.yaml file --- assets/settings.yaml | 27 +++++++++------------------ cloud/client.go | 3 ++- common/commander.go | 3 +++ common/version.go | 2 +- conda/download.go | 4 +++- conda/workflows.go | 2 +- docs/changelog.md | 6 ++++++ operations/assistant.go | 3 ++- operations/community.go | 4 +++- settings/data.go | 38 ++++++++++++++++++++++---------------- settings/settings.go | 20 +++++++++++++++++++- 11 files changed, 71 insertions(+), 41 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index ed78da4f..b9477c60 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,36 +1,27 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://id.robocorp.com - pypi: https://pypi.org/simple/ - pypi-files: https://files.pythonhosted.org - pypi-trusted: - conda: https://repo.anaconda.org + pypi: # https://pypi.org/simple/ + pypi-trusted: # https://pypi.org/simple/ + conda: # https://repo.anaconda.org downloads: https://downloads.robocorp.com docs: https://robocorp.com/docs/ telemetry: https://telemetry.robocorp.com issues: https://telemetry.robocorp.com - portal: https://robocorp.com/portal/ - robot-pull: https://github.com -proxies: - http: - https: +diagnostics-hosts: + - files.pythonhosted.org + - github.com + - repo.anaconda.org + - pypi.org autoupdates: assistant: https://downloads.robocorp.com/assistant/releases - workforce-agent: https://downloads.robocorp.com/agent/releases + workforce-agent: https://downloads.robocorp.com/workforce-agent/releases lab: https://downloads.code.robocorp.com/lab/installer certificates: verify-ssl: true - root-location: - -logs: - level: normal - root-location: "%localappdata%\\robocorp\\logs" - -business-data: - root-location: "%HOMEDRIVE%%HOMEPATH%\\Documents" branding: logo: https://downloads.robocorp.com/company/press-kit/logos/robocorp-logo-black.svg diff --git a/cloud/client.go b/cloud/client.go index 106e235e..76130687 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -57,7 +58,7 @@ func NewClient(endpoint string) (Client, error) { } return &internalClient{ endpoint: https, - client: &http.Client{}, + client: &http.Client{Transport: settings.Global.ConfiguredHttpTransport()}, }, nil } diff --git a/common/commander.go b/common/commander.go index 561a3145..5f5d77b4 100644 --- a/common/commander.go +++ b/common/commander.go @@ -1,10 +1,13 @@ package common +import "strings" + type Commander struct { command []string } func (it *Commander) Option(name, value string) *Commander { + value = strings.TrimSpace(value) if len(value) > 0 { it.command = append(it.command, name, value) } diff --git a/common/version.go b/common/version.go index 317a3a0f..9533efa7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.2` + Version = `v9.8.3` ) diff --git a/conda/download.go b/conda/download.go index 50179945..c5cd8adb 100644 --- a/conda/download.go +++ b/conda/download.go @@ -10,13 +10,15 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/settings" ) func DownloadMicromamba() error { common.Timeline("downloading micromamba") url := MicromambaLink() filename := BinMicromamba() - response, err := http.Get(url) + client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} + response, err := client.Get(url) if err != nil { return err } diff --git a/conda/workflows.go b/conda/workflows.go index 831e73c5..500d69d8 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -188,7 +188,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh ttl = "0" } mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder) - //mambaCommand.Option("--channel", settings.Global.CondaURL()) + mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") common.Timeline("Micromamba start.") diff --git a/docs/changelog.md b/docs/changelog.md index 87ff68ab..abf1ef06 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.8.3 (date: 24.3.2021) + +- can configure all rcc operations not to verify correct SSL certificate + (please note, doing this is insecure and allows man-in-the-middle attacks) +- applied reviewed changes to what is actually in settings.yaml file + ## v9.8.2 (date: 23.3.2021) - ALPHA level pre-release (do not use, unless you know what you are doing) diff --git a/operations/assistant.go b/operations/assistant.go index 402090c8..2867c34b 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -18,6 +18,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/settings" ) const ( @@ -188,7 +189,7 @@ func MultipartUpload(url string, fields map[string]string, basename, fullpath st return err } request.Header.Add("Content-Type", many.FormDataContentType()) - client := &http.Client{} + client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} response, err := client.Do(request) if err != nil { return err diff --git a/operations/community.go b/operations/community.go index 8b8a9a4c..bd3d29a8 100644 --- a/operations/community.go +++ b/operations/community.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" ) const ( @@ -46,7 +47,8 @@ func CommunityLocation(name, branch string) string { } func DownloadCommunityRobot(url, filename string) error { - response, err := http.Get(url) + client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} + response, err := client.Get(url) if err != nil { return err } diff --git a/settings/data.go b/settings/data.go index 333883d2..71f9a03c 100644 --- a/settings/data.go +++ b/settings/data.go @@ -19,9 +19,9 @@ type StringMap map[string]string type Settings struct { Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` Branding StringMap `yaml:"branding" json:"branding"` - BusinessData *BusinessData `yaml:"business-data" json:"business-data"` Certificates *Certificates `yaml:"certificates" json:"certificates"` Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` + Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` Logs *Logs `yaml:"logs" json:"logs"` Meta *Meta `yaml:"meta" json:"meta"` Proxies *Proxies `yaml:"proxies" json:"proxies"` @@ -51,6 +51,26 @@ func (it *Settings) Source(filename string) *Settings { return it } +func (it *Settings) Hostnames() []string { + collector := make(map[string]bool) + if it.Endpoints != nil { + for _, name := range it.Endpoints.Hostnames() { + collector[name] = true + } + } + if it.Hosts != nil { + for _, name := range it.Hosts { + collector[name] = true + } + } + result := make([]string, 0, len(collector)) + for key, _ := range collector { + result = append(result, key) + } + sort.Strings(result) + return result +} + func (it *Settings) AsJson() ([]byte, error) { content, err := json.MarshalIndent(it, "", " ") if err != nil { @@ -84,8 +104,6 @@ func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStat correct = false } else { correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) } if correct { @@ -96,10 +114,6 @@ func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStat func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { diagnose := target.Diagnose("Settings") correct := true - if it.BusinessData == nil { - diagnose.Warning("", "settings.yaml: business-data section is totally missing") - correct = false - } if it.Certificates == nil { diagnose.Warning("", "settings.yaml: certificates section is totally missing") correct = false @@ -125,10 +139,6 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { } } -type BusinessData struct { - RootLocation string `yaml:"root-location" json:"root-location"` -} - type Certificates struct { VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` RootLocation string `yaml:"root-location" json:"root-location"` @@ -138,14 +148,12 @@ type Endpoints struct { CloudApi string `yaml:"cloud-api" json:"cloud-api"` CloudLinking string `yaml:"cloud-linking" json:"cloud-linking"` Conda string `yaml:"conda" json:"conda"` - Docs string `yaml:"docs" json:"docs"` Downloads string `yaml:"downloads" json:"downloads"` Issues string `yaml:"issues" json:"issues"` Portal string `yaml:"portal" json:"portal"` Pypi string `yaml:"pypi" json:"pypi"` PypiFiles string `yaml:"pypi-files" json:"pypi-files"` PypiTrusted string `yaml:"pypi-trusted" json:"pypi-trusted"` - RobotPull string `yaml:"robot-pull" json:"robot-pull"` Telemetry string `yaml:"telemetry" json:"telemetry"` } @@ -161,19 +169,17 @@ func hostFromUrl(link string, collector map[string]bool) { collector[parts[0]] = true } -func (it *Endpoints) Hosts() []string { +func (it *Endpoints) Hostnames() []string { collector := make(map[string]bool) hostFromUrl(it.CloudApi, collector) hostFromUrl(it.CloudLinking, collector) hostFromUrl(it.Conda, collector) - hostFromUrl(it.Docs, collector) hostFromUrl(it.Downloads, collector) hostFromUrl(it.Issues, collector) hostFromUrl(it.Portal, collector) hostFromUrl(it.Pypi, collector) hostFromUrl(it.PypiFiles, collector) hostFromUrl(it.PypiTrusted, collector) - hostFromUrl(it.RobotPull, collector) hostFromUrl(it.Telemetry, collector) result := make([]string, 0, len(collector)) for key, _ := range collector { diff --git a/settings/settings.go b/settings/settings.go index af2b9479..3d2bc4a7 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -1,9 +1,11 @@ package settings import ( + "crypto/tls" "fmt" "io" "io/ioutil" + "net/http" "os" "path/filepath" @@ -14,6 +16,7 @@ import ( ) var ( + httpTransport *http.Transport cachedSettings *Settings Global gateway ) @@ -125,9 +128,24 @@ func (it gateway) DownloadsURL() string { } func (it gateway) Hostnames() []string { - return it.Endpoints().Hosts() + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Hostnames() +} + +func (it gateway) ConfiguredHttpTransport() *http.Transport { + return httpTransport } func init() { + verifySsl := true Global = gateway(true) + httpTransport = http.DefaultTransport.(*http.Transport).Clone() + settings, err := SummonSettings() + if err == nil && settings.Certificates != nil { + verifySsl = settings.Certificates.VerifySsl + } + if !verifySsl { + httpTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } } From af55b7bfb4c883665f4e2a87eedb45cdc60740a2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Mar 2021 14:59:43 +0200 Subject: [PATCH 094/516] RCC-158: settings.yaml (v9.8.4) --- common/version.go | 2 +- conda/workflows.go | 6 +++++- docs/changelog.md | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 9533efa7..06e99eb3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.3` + Version = `v9.8.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 500d69d8..0c523558 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -175,6 +175,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } defer func() { planWriter.Close() + content, err := ioutil.ReadFile(planfile) + if err == nil { + common.Log("%s", string(content)) + } os.Remove(planfile) }() fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) @@ -214,7 +218,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Log("#### Progress: 4/6 [pip install phase]") common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) - pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText, "--quiet") + pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) common.Debug("=== new live --- pip install phase ===") err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) diff --git a/docs/changelog.md b/docs/changelog.md index abf1ef06..52e97d82 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.8.4 (date: 24.3.2021) + +- fix for pip made too silent on this v9.8.x series +- and also in failure cases, print out full installation plan + ## v9.8.3 (date: 24.3.2021) - can configure all rcc operations not to verify correct SSL certificate From 65be18a7d1c02dece0409eec592ae1be1477ebce Mon Sep 17 00:00:00 2001 From: Kari Harju <56814402+kariharju@users.noreply.github.com> Date: Thu, 25 Mar 2021 11:25:42 +0200 Subject: [PATCH 095/516] Template updates (#16) * Template updates: - Updated rpaframework to v9.1.0 - Improved extended template - robot.yaml commands to shell format and better tasks names. * v9.8.5 --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ templates/extended/conda.yaml | 2 +- templates/extended/robot.yaml | 17 +++++------------ templates/extended/tasks.robot | 11 ++++++++--- templates/extended/variables/MyVariables.py | 3 +++ templates/python/conda.yaml | 2 +- templates/python/robot.yaml | 6 ++---- templates/standard/conda.yaml | 2 +- templates/standard/robot.yaml | 14 ++------------ 10 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 templates/extended/variables/MyVariables.py diff --git a/common/version.go b/common/version.go index 06e99eb3..330bb691 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.4` + Version = `v9.8.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 52e97d82..cd8a196c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.8.5 (date: 24.3.2021) + +- Robot templates updated: Rpaframework updated to v9.1.0 +- Robot templates updated: Improved task names +- Robot templates updated: Extended template has example of multiple tasks execution + ## v9.8.4 (date: 24.3.2021) - fix for pip made too silent on this v9.8.x series diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index e6db2250..be963fe6 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/extended/robot.yaml b/templates/extended/robot.yaml index 664eaa66..0cd1d454 100755 --- a/templates/extended/robot.yaml +++ b/templates/extended/robot.yaml @@ -1,16 +1,9 @@ tasks: - Default: - command: - - python - - -m - - robot - - --report - - NONE - - --outputdir - - output - - --logtitle - - Task log - - tasks.robot + Run all tasks: + shell: python -m robot --report NONE --outputdir output --logtitle "Task log" tasks.robot + + Run Example task: + robotTaskName: Example Task condaConfigFile: conda.yaml ignoreFiles: diff --git a/templates/extended/tasks.robot b/templates/extended/tasks.robot index 61112764..f1607b72 100644 --- a/templates/extended/tasks.robot +++ b/templates/extended/tasks.robot @@ -1,11 +1,16 @@ *** Settings *** Documentation Template robot main suite. -Resource keywords.robot +Library Collections + Library MyLibrary -Variables variables.py +Resource keywords.robot +Variables MyVariables.py *** Tasks *** -Example task +Example Task Example Keyword Example Python Keyword + Log ${TODAY} + + diff --git a/templates/extended/variables/MyVariables.py b/templates/extended/variables/MyVariables.py new file mode 100644 index 00000000..d398aff1 --- /dev/null +++ b/templates/extended/variables/MyVariables.py @@ -0,0 +1,3 @@ +from datetime import datetime + +TODAY = datetime.now() diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index e6db2250..be963fe6 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/robot.yaml b/templates/python/robot.yaml index 56b9080a..f78b7601 100644 --- a/templates/python/robot.yaml +++ b/templates/python/robot.yaml @@ -1,8 +1,6 @@ tasks: - Default: - command: - - python - - task.py + Run Python: + shell: python task.py condaConfigFile: conda.yaml artifactsDir: output diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index e6db2250..be963fe6 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==7.6.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/robot.yaml b/templates/standard/robot.yaml index a04af300..5c3786e2 100644 --- a/templates/standard/robot.yaml +++ b/templates/standard/robot.yaml @@ -1,16 +1,6 @@ tasks: - Default: - command: - - python - - -m - - robot - - --report - - NONE - - --outputdir - - output - - --logtitle - - Task log - - tasks.robot + Run all tasks: + shell: python -m robot --report NONE --outputdir output --logtitle "Task log" tasks.robot condaConfigFile: conda.yaml artifactsDir: output From 6972c13e5480605e9dedf3345bcfcc4b9e797ee0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 25 Mar 2021 12:51:20 +0200 Subject: [PATCH 096/516] RCC-158: settings.yaml (v9.8.6) - settings.yaml cleanup - fixed robot tests for 9.8.5 template changes --- assets/settings.yaml | 8 ++++---- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot_tests/fullrun.robot | 12 ++++++------ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index b9477c60..89159f0e 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,11 +1,11 @@ endpoints: - cloud-api: https://api.eu1.robocorp.com/ + cloud-api: https://api.eu1.robocorp.com cloud-linking: https://id.robocorp.com - pypi: # https://pypi.org/simple/ - pypi-trusted: # https://pypi.org/simple/ + pypi: # https://pypi.org/simple + pypi-trusted: # https://pypi.org conda: # https://repo.anaconda.org downloads: https://downloads.robocorp.com - docs: https://robocorp.com/docs/ + docs: https://robocorp.com/docs telemetry: https://telemetry.robocorp.com issues: https://telemetry.robocorp.com diff --git a/common/version.go b/common/version.go index 330bb691..cc842a66 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.5` + Version = `v9.8.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index cd8a196c..ea945843 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.8.6 (date: 25.3.2021) + +- settings.yaml cleanup +- fixed robot tests for 9.8.5 template changes + ## v9.8.5 (date: 24.3.2021) - Robot templates updated: Rpaframework updated to v9.1.0 diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 8c3fd0ba..f656a2bb 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -75,8 +75,8 @@ Using and running template example with shell file Must Have fluffy is not empty Goal Run task in place. - Step build/rcc task run --controller citests -r tmp/fluffy/robot.yaml - Must Have 1 critical task, 1 passed, 0 failed + Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml + Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have Progress: 0/6 Must Have Progress: 1/6 @@ -89,8 +89,8 @@ Using and running template example with shell file Must Exist %{ROBOCORP_HOME}/pipcache/ Goal Run task in clean temporary directory. - Step build/rcc task testrun --controller citests -r tmp/fluffy/robot.yaml - Must Have 1 critical task, 1 passed, 0 failed + Step build/rcc task testrun --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml + Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework Must Have Progress: 0/6 @@ -143,7 +143,7 @@ Using and running template example with shell file Must Be Json Response Goal See variables from specific environment with robot.yaml knowledge - Step build/rcc env variables --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json + Step build/rcc env variables --task "Run Example task" --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc @@ -169,7 +169,7 @@ Using and running template example with shell file Wont Have RC_WORKSPACE_ID= Goal See variables from specific environment with robot.yaml knowledge in JSON form - Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json + Step build/rcc env variables --task "Run Example task" --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Be Json Response Goal See diagnostics as valid JSON form From e99999002367da54b110a327156861eb8c660317 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 26 Mar 2021 12:54:00 +0200 Subject: [PATCH 097/516] RCC-158: settings.yaml (v9.8.7) - more finalization of settings.yaml change - made micromamba less quiet on environment building - secrets now have write access enabled in rcc authorization requests - if merged conda.yaml files do not have names, merge result wont have either --- assets/settings.yaml | 24 ++--- common/version.go | 2 +- conda/condayaml.go | 4 +- conda/condayaml_test.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/validate.go | 8 +- conda/workflows.go | 3 +- docs/changelog.md | 7 ++ go.mod | 3 +- go.sum | 178 ++++++++++++++++++++++++++++---- operations/authorize.go | 2 +- operations/authorize_test.go | 2 +- operations/diagnostics.go | 26 +++-- robot_tests/fullrun.robot | 4 +- settings/data.go | 62 +++++------ settings/settings.go | 31 +++++- settings/settings_test.go | 3 + xviper/wrapper.go | 6 +- 20 files changed, 280 insertions(+), 93 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index 89159f0e..fd567ebd 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,13 +1,13 @@ endpoints: - cloud-api: https://api.eu1.robocorp.com - cloud-linking: https://id.robocorp.com - pypi: # https://pypi.org/simple - pypi-trusted: # https://pypi.org - conda: # https://repo.anaconda.org - downloads: https://downloads.robocorp.com - docs: https://robocorp.com/docs - telemetry: https://telemetry.robocorp.com - issues: https://telemetry.robocorp.com + cloud-api: https://api.eu1.robocorp.com/ + cloud-linking: https://id.robocorp.com/ + pypi: # https://pypi.org/simple/ + pypi-trusted: # https://pypi.org/ + conda: # https://repo.anaconda.org/ + downloads: https://downloads.robocorp.com/ + docs: https://robocorp.com/docs/ + telemetry: https://telemetry.robocorp.com/ + issues: https://telemetry.robocorp.com/ diagnostics-hosts: - files.pythonhosted.org @@ -16,9 +16,9 @@ diagnostics-hosts: - pypi.org autoupdates: - assistant: https://downloads.robocorp.com/assistant/releases - workforce-agent: https://downloads.robocorp.com/workforce-agent/releases - lab: https://downloads.code.robocorp.com/lab/installer + assistant: https://downloads.robocorp.com/assistant/releases/ + workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ + lab: https://downloads.code.robocorp.com/lab/installer/ certificates: verify-ssl: true diff --git a/common/version.go b/common/version.go index cc842a66..fc815d7e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.6` + Version = `v9.8.7` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 27781ec0..95920215 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -223,7 +223,9 @@ func pushPip(target *Environment, dependencies []*Dependency) error { func (it *Environment) Merge(right *Environment) (*Environment, error) { result := new(Environment) - result.Name = it.Name + "+" + right.Name + if len(it.Name) > 0 || len(right.Name) > 0 { + result.Name = it.Name + "+" + right.Name + } seenChannels := make(map[string]bool) result.Channels = addItem(seenChannels, it.Channels, result.Channels) diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index fc19822c..c944680e 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -98,7 +98,7 @@ func TestCanMergeTwoEnvironments(t *testing.T) { sut, err := left.Merge(right) must_be.Nil(err) wont_be.Nil(sut) - must_be.Equal("+", sut.Name) + must_be.Equal("", sut.Name) must_be.Equal(2, len(sut.Channels)) must_be.Equal(4, len(sut.Conda)) must_be.Equal(1, len(sut.Pip)) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index f1e6158c..2482304e 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/macos64/micromamba" + return settings.Global.DownloadsLink("micromamba/v0.8.2/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index c177c72d..60b8f22d 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/linux64/micromamba" + return settings.Global.DownloadsLink("micromamba/v0.8.2/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index b22ff767..07b9c8b3 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsURL() + "/micromamba/v0.8.2/windows64/micromamba.exe" + return settings.Global.DownloadsLink("micromamba/v0.8.2/windows64/micromamba.exe") } var ( diff --git a/conda/validate.go b/conda/validate.go index f75f577d..2fd4ccda 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -5,14 +5,14 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" ) -const ( - longPathSupportArticle = `https://robocorp.com/docs/product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on` -) +const () var ( - validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") + longPathSupportArticle = settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") + validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") ) func ValidLocation(value string) bool { diff --git a/conda/workflows.go b/conda/workflows.go index 0c523558..451e23f4 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -191,7 +191,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-q", "-y", "-f", condaYaml, "-p", targetFolder) + mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") @@ -220,6 +220,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) + pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) common.Debug("=== new live --- pip install phase ===") err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index ea945843..c24c7d76 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.8.7 (date: 26.3.2021) + +- more finalization of settings.yaml change +- made micromamba less quiet on environment building +- secrets now have write access enabled in rcc authorization requests +- if merged conda.yaml files do not have names, merge result wont have either + ## v9.8.6 (date: 25.3.2021) - settings.yaml cleanup diff --git a/go.mod b/go.mod index f0ab850b..9247637c 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,8 @@ require ( github.com/spf13/cobra v0.0.7 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.6.2 + github.com/spf13/viper v1.7.1 golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d - golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 gopkg.in/yaml.v2 v2.2.8 diff --git a/go.sum b/go.sum index 4b537b69..6d2e485c 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,53 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M= github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -36,23 +57,56 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -65,28 +119,38 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -97,7 +161,10 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -106,31 +173,28 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= -github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -138,52 +202,125 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -192,9 +329,12 @@ gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+p gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/operations/authorize.go b/operations/authorize.go index b6e6c353..d84acd65 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -124,7 +124,7 @@ func RobotClaims(seconds int, workspace string) *Claims { func RunClaims(seconds int, workspace string) *Claims { result := NewClaims("Run", fmt.Sprintf(WorkspaceApi, workspace), seconds) - result.Capabilities.Add("secret", true, true, false) + result.Capabilities.Add("secret", true, true, true) result.Capabilities.Add("artifact", false, false, true) result.Capabilities.Add("livedata", false, true, true) result.Capabilities.Add("workitemdata", false, true, true) diff --git a/operations/authorize_test.go b/operations/authorize_test.go index 7cd30dbb..4ec6465c 100644 --- a/operations/authorize_test.go +++ b/operations/authorize_test.go @@ -110,7 +110,7 @@ func TestCanCreateRunClaims(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) setup := operations.NewClaims("Run", "https://some.com", 88) - setup.Capabilities.Add("secret", true, true, false) + setup.Capabilities.Add("secret", true, true, true) setup.Capabilities.Add("artifact", false, false, true) setup.Capabilities.Add("livedata", false, true, true) setup.Capabilities.Add("workitemdata", false, true, true) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 8cb32670..2d6d995c 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -26,14 +26,17 @@ import ( ) const ( - canaryUrl = `/canary.txt` - supportLongPathUrl = `https://robocorp.com/docs/troubleshooting/windows-long-path` - supportNetworkUrl = `https://robocorp.com/docs/troubleshooting/firewall-and-proxies` - supportGeneralUrl = `https://robocorp.com/docs/troubleshooting` - statusOk = `ok` - statusWarning = `warning` - statusFail = `fail` - statusFatal = `fatal` + canaryUrl = `/canary.txt` + statusOk = `ok` + statusWarning = `warning` + statusFail = `fail` + statusFatal = `fatal` +) + +var ( + supportLongPathUrl = settings.Global.DocsLink("troubleshooting/windows-long-path") + supportNetworkUrl = settings.Global.DocsLink("troubleshooting/firewall-and-proxies") + supportGeneralUrl = settings.Global.DocsLink("troubleshooting") ) type stringerr func() (string, error) @@ -144,12 +147,12 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { } func canaryDownloadCheck() *common.DiagnosticCheck { - client, err := cloud.NewClient(settings.Global.DownloadsURL()) + client, err := cloud.NewClient(settings.Global.DownloadsLink("")) if err != nil { return &common.DiagnosticCheck{ Type: "network", Status: statusFail, - Message: fmt.Sprintf("%v: %v", settings.Global.DownloadsURL(), err), + Message: fmt.Sprintf("%v: %v", settings.Global.DownloadsLink(""), err), Link: supportNetworkUrl, } } @@ -166,7 +169,7 @@ func canaryDownloadCheck() *common.DiagnosticCheck { return &common.DiagnosticCheck{ Type: "network", Status: statusOk, - Message: fmt.Sprintf("Canary download successful: %s%s", settings.Global.DownloadsURL(), canaryUrl), + Message: fmt.Sprintf("Canary download successful: %s", settings.Global.DownloadsLink(canaryUrl)), Link: supportNetworkUrl, } } @@ -218,6 +221,7 @@ func ProduceDiagnostics(filename, robotfile string, json bool) (*common.Diagnost if len(robotfile) > 0 { addRobotDiagnostics(robotfile, result) } + settings.Global.Diagnostics(result) if json { jsonDiagnostics(file, result) } else { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index f656a2bb..ceb4ec05 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -109,11 +109,11 @@ Using and running template example with shell file Goal Merge two different conda.yaml files without conflict passes Step build/rcc env new --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent - Must Have 786f01e87dc8d6e6 + Must Have 0cc761cfb9692a36 Goal Can list environments as JSON Step build/rcc env list --controller citests --json - Must Have 786f01e87dc8d6e6 + Must Have 0cc761cfb9692a36 Must Be Json Response Goal See variables from specific environment without robot.yaml knowledge diff --git a/settings/data.go b/settings/data.go index 71f9a03c..b3dc2e8d 100644 --- a/settings/data.go +++ b/settings/data.go @@ -22,9 +22,7 @@ type Settings struct { Certificates *Certificates `yaml:"certificates" json:"certificates"` Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` - Logs *Logs `yaml:"logs" json:"logs"` Meta *Meta `yaml:"meta" json:"meta"` - Proxies *Proxies `yaml:"proxies" json:"proxies"` } func FromBytes(raw []byte) (*Settings, error) { @@ -121,52 +119,65 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { if it.Endpoints == nil { diagnose.Warning("", "settings.yaml: endpoints section is totally missing") correct = false - } - if it.Logs == nil { - diagnose.Warning("", "settings.yaml: logs section is totally missing") - correct = false + } else { + correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.CloudLinking, "endpoints/cloud-linking", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Docs, "endpoints/docs", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Issues, "endpoints/issues", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Telemetry, "endpoints/telemetry", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) + if len(it.Endpoints.Conda) > 0 { + correct = diagnoseUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) + } + if len(it.Endpoints.Pypi) > 0 { + correct = diagnoseUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) + } + if len(it.Endpoints.PypiTrusted) > 0 { + correct = diagnoseUrl(it.Endpoints.PypiTrusted, "endpoints/pypi-trusted", diagnose, correct) + } } if it.Meta == nil { diagnose.Warning("", "settings.yaml: meta section is totally missing") correct = false } - if it.Proxies == nil { - diagnose.Warning("", "settings.yaml: proxies section is totally missing") - correct = false - } if correct { diagnose.Ok("Toplevel settings are ok.") } } type Certificates struct { - VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` - RootLocation string `yaml:"root-location" json:"root-location"` + VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` } type Endpoints struct { CloudApi string `yaml:"cloud-api" json:"cloud-api"` CloudLinking string `yaml:"cloud-linking" json:"cloud-linking"` Conda string `yaml:"conda" json:"conda"` + Docs string `yaml:"docs" json:"docs"` Downloads string `yaml:"downloads" json:"downloads"` Issues string `yaml:"issues" json:"issues"` - Portal string `yaml:"portal" json:"portal"` Pypi string `yaml:"pypi" json:"pypi"` - PypiFiles string `yaml:"pypi-files" json:"pypi-files"` PypiTrusted string `yaml:"pypi-trusted" json:"pypi-trusted"` Telemetry string `yaml:"telemetry" json:"telemetry"` } -func hostFromUrl(link string, collector map[string]bool) { +func justHostAndPort(link string) string { if len(link) == 0 { - return + return "" } parsed, err := url.Parse(link) if err != nil { - return + return "" + } + return parsed.Host +} + +func hostFromUrl(link string, collector map[string]bool) { + host := justHostAndPort(link) + if len(host) > 0 { + parts := strings.SplitN(host, ":", 2) + collector[parts[0]] = true } - parts := strings.SplitN(parsed.Host, ":", 2) - collector[parts[0]] = true } func (it *Endpoints) Hostnames() []string { @@ -174,11 +185,10 @@ func (it *Endpoints) Hostnames() []string { hostFromUrl(it.CloudApi, collector) hostFromUrl(it.CloudLinking, collector) hostFromUrl(it.Conda, collector) + hostFromUrl(it.Docs, collector) hostFromUrl(it.Downloads, collector) hostFromUrl(it.Issues, collector) - hostFromUrl(it.Portal, collector) hostFromUrl(it.Pypi, collector) - hostFromUrl(it.PypiFiles, collector) hostFromUrl(it.PypiTrusted, collector) hostFromUrl(it.Telemetry, collector) result := make([]string, 0, len(collector)) @@ -189,17 +199,7 @@ func (it *Endpoints) Hostnames() []string { return result } -type Logs struct { - Level string `yaml:"level" json:"level"` - RootLocation string `yaml:"root-location" json:"root-location"` -} - type Meta struct { Source string `yaml:"source" json:"source"` Version string `yaml:"version" json:"version"` } - -type Proxies struct { - Http string `yaml:"http" json:"http"` - Https string `yaml:"https" json:"https"` -} diff --git a/settings/settings.go b/settings/settings.go index 3d2bc4a7..5e6ea250 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "path/filepath" @@ -94,8 +95,26 @@ func CriticalEnvironmentSettingsCheck() { } } +func resolveLink(link, page string) string { + docs, err := url.Parse(link) + if err != nil { + return page + } + local, err := url.Parse(page) + if err != nil { + return page + } + return docs.ResolveReference(local).String() +} + type gateway bool +func (it gateway) Diagnostics(target *common.DiagnosticStatus) { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + config.Diagnostics(target) +} + func (it gateway) Endpoints() *Endpoints { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) @@ -119,12 +138,20 @@ func (it gateway) PypiURL() string { return it.Endpoints().Pypi } +func (it gateway) PypiTrustedHost() string { + return justHostAndPort(it.Endpoints().PypiTrusted) +} + func (it gateway) CondaURL() string { return it.Endpoints().Conda } -func (it gateway) DownloadsURL() string { - return it.Endpoints().Downloads +func (it gateway) DownloadsLink(resource string) string { + return resolveLink(it.Endpoints().Downloads, resource) +} + +func (it gateway) DocsLink(page string) string { + return resolveLink(it.Endpoints().Docs, page) } func (it gateway) Hostnames() []string { diff --git a/settings/settings_test.go b/settings/settings_test.go index 1932069f..96347f98 100644 --- a/settings/settings_test.go +++ b/settings/settings_test.go @@ -16,4 +16,7 @@ func TestCanCallEntropyFunction(t *testing.T) { wont_be.Nil(settings.Global) must_be.True(len(settings.Global.Hostnames()) > 1) + + must_be.Equal("https://robocorp.com/docs/hello.html", settings.Global.DocsLink("hello.html")) + must_be.Equal("https://robocorp.com/docs/products/manual.html", settings.Global.DocsLink("products/manual.html")) } diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 0c6a7d13..1ef8facf 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -47,7 +47,11 @@ func (it *config) Save() { defer locker.Release() defer os.Remove(it.Lockfile) - it.Viper.WriteConfigAs(it.Filename) + err = it.Viper.WriteConfigAs(it.Filename) + if err != nil { + common.Log("FATAL: could not write %v, reason %v; ignored.", it.Filename, err) + return + } when, err := pathlib.Modtime(it.Filename) if err == nil { it.Timestamp = when From 7ed8d89b2a6c5dbcce88eae70aefe52b7afab645 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 29 Mar 2021 17:41:21 +0300 Subject: [PATCH 098/516] RCC-159: internal ecc experiment (v9.8.8) - mixed fixes and experiments edition - ignoring empty variable names on environment dumps, closes #17 - added some missing content types to web requests - added experimental ephemeral ECC implementation - more common timeline markers added - will not list pip dependencies on assistant runs - will not ask cloud for runtime authorization (bug fix) --- cmd/assistantRun.go | 11 ++- cmd/carrier.go | 2 +- cmd/internal.go | 1 + cmd/internalEnv.go | 2 +- cmd/internale2ee.go | 90 +++++++++++++++------ cmd/run.go | 5 +- cmd/sharedvariables.go | 1 + cmd/shell.go | 2 +- cmd/testrun.go | 2 +- cmd/variables.go | 3 + common/version.go | 2 +- conda/validate.go | 2 - docs/changelog.md | 10 +++ go.mod | 1 + go.sum | 3 + operations/assistant.go | 10 ++- operations/{security.go => encryptionv1.go} | 19 ++--- operations/encryptionv2.go | 78 ++++++++++++++++++ operations/running.go | 9 ++- operations/security_test.go | 40 ++++++++- operations/workspaces.go | 1 + shell/task.go | 1 + 22 files changed, 241 insertions(+), 54 deletions(-) rename operations/{security.go => encryptionv1.go} (90%) create mode 100644 operations/encryptionv2.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 4f341179..57a31f33 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -48,7 +48,9 @@ var assistantRunCmd = &cobra.Command{ defer func() { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) }() - assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId) + common.Timeline("start assistant run cloud call started") + assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId, useEcc) + common.Timeline("start assistant run cloud call completed") if err != nil { pretty.Exit(3, "Could not run assistant, reason: %v", err) } @@ -110,12 +112,16 @@ var assistantRunCmd = &cobra.Command{ cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.Elapsed().String()) }() reason = "ROBOT_FAILURE" - operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) + operations.SelectExecutionModel(captureRunFlags(true), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) pretty.Ok() status, reason = "OK", "PASS" }, } +var ( + useEcc bool +) + func init() { assistantCmd.AddCommand(assistantRunCmd) assistantRunCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to get assistant information.") @@ -123,4 +129,5 @@ func init() { assistantRunCmd.Flags().StringVarP(&assistantId, "assistant", "a", "", "Assistant id to execute.") assistantRunCmd.MarkFlagRequired("assistant") assistantRunCmd.Flags().StringVarP(©Directory, "copy", "c", "", "Location to copy changed artifacts from run (optional).") + assistantRunCmd.Flags().BoolVarP(&useEcc, "ecc", "", false, "DO NOT USE! INTERNAL EXPERIMENT!") } diff --git a/cmd/carrier.go b/cmd/carrier.go index a79456b5..19550378 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -71,7 +71,7 @@ func runCarrier() error { simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) - operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) + operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, false, nil) return nil } diff --git a/cmd/internal.go b/cmd/internal.go index 9ef329f8..3faa6b3e 100644 --- a/cmd/internal.go +++ b/cmd/internal.go @@ -14,4 +14,5 @@ var internalCmd = &cobra.Command{ func init() { rootCmd.AddCommand(internalCmd) + internalCmd.PersistentFlags().StringVarP(&wskey, "wskey", "", "", "Cloud API workspace key (authorization).") } diff --git a/cmd/internalEnv.go b/cmd/internalEnv.go index f3f2974e..936c5717 100644 --- a/cmd/internalEnv.go +++ b/cmd/internalEnv.go @@ -20,7 +20,7 @@ var internalEnvCmd = &cobra.Command{ values := make(map[string]string) for _, entry := range os.Environ() { parts := strings.SplitN(entry, "=", 2) - if len(parts) == 2 { + if len(parts) == 2 && len(parts[0]) > 0 { values[parts[0]] = parts[1] } } diff --git a/cmd/internale2ee.go b/cmd/internale2ee.go index 637d3de9..d0ca2c2c 100644 --- a/cmd/internale2ee.go +++ b/cmd/internale2ee.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" @@ -9,6 +11,10 @@ import ( "github.com/spf13/cobra" ) +var ( + encryptionVersion int +) + var e2eeCmd = &cobra.Command{ Use: "encryption", Short: "Internal end-to-end encryption tester method", @@ -18,37 +24,71 @@ var e2eeCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Encryption lasted").Report() } - account := operations.AccountByName(AccountName()) - if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) - } - client, err := cloud.NewClient(account.Endpoint) - if err != nil { - pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) - } - key, err := operations.GenerateEphemeralKey() - if err != nil { - pretty.Exit(3, "Problem with key generation, reason: %v", err) - } - request := client.NewRequest("/assistant-v1/test/encryption") - request.Body, err = key.RequestBody(args[0]) - if err != nil { - pretty.Exit(4, "Problem with body generation, reason: %v", err) + if encryptionVersion == 1 { + version1encryption(args) + } else { + version2encryption(args) } - response := client.Post(request) - if response.Status != 200 { - pretty.Exit(5, "Problem with test request, status=%d, body=%s", response.Status, response.Body) - } - plaintext, err := key.Decode(response.Body) - if err != nil { - pretty.Exit(6, "Decode problem with body %s, reason: %v", response.Body, err) - } - common.Log("Response: %s", string(plaintext)) pretty.Ok() }, } +func version1encryption(args []string) { + account := operations.AccountByName(AccountName()) + pretty.Guard(account != nil, 1, "Could not find account by name: %v", AccountName()) + + client, err := cloud.NewClient(account.Endpoint) + pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) + + key, err := operations.GenerateEphemeralKey() + pretty.Guard(err == nil, 3, "Problem with key generation, reason: %v", err) + + request := client.NewRequest("/assistant-v1/test/encryption") + request.Body, err = key.RequestBody(args[0]) + pretty.Guard(err == nil, 4, "Problem with body generation, reason: %v", err) + + response := client.Post(request) + pretty.Guard(response.Status == 200, 5, "Problem with test request, status=%d, body=%s", response.Status, response.Body) + + plaintext, err := key.Decode(response.Body) + pretty.Guard(err == nil, 6, "Decode problem with body %s, reason: %v", response.Body, err) + + common.Log("Response: %s", string(plaintext)) +} + +func version2encryption(args []string) { + account := operations.AccountByName(AccountName()) + pretty.Guard(account != nil, 1, "Could not find account by name: %v", AccountName()) + + client, err := cloud.NewClient(account.Endpoint) + pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) + + key, err := operations.GenerateEphemeralEccKey() + pretty.Guard(err == nil, 3, "Problem with key generation, reason: %v", err) + + location := fmt.Sprintf("/assistant-v1/workspaces/%s/assistants/%s/test", workspaceId, assistantId) + request := client.NewRequest(location) + request.Headers["Authorization"] = fmt.Sprintf("RC-WSKEY %s", wskey) + request.Body, err = key.RequestBody(nil) + pretty.Guard(err == nil, 4, "Problem with body generation, reason: %v", err) + + common.Timeline("POST to cloud started") + response := client.Post(request) + common.Timeline("POST done") + pretty.Guard(response.Status == 200, 5, "Problem with test request, status=%d, body=%s", response.Status, response.Body) + + common.Timeline("decode start") + plaintext, err := key.Decode(response.Body) + common.Timeline("decode done") + pretty.Guard(err == nil, 6, "Decode problem with body %s, reason: %v", response.Body, err) + + common.Log("Response: %s", string(plaintext)) +} + func init() { internalCmd.AddCommand(e2eeCmd) e2eeCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Cloud operations.") + e2eeCmd.Flags().IntVarP(&encryptionVersion, "use", "u", 1, "Which version of encryption method to test (1 or 2)") + e2eeCmd.Flags().StringVarP(&workspaceId, "workspace", "", "", "Workspace id to get assistant information.") + e2eeCmd.Flags().StringVarP(&assistantId, "assistant", "", "", "Assistant id to execute.") } diff --git a/cmd/run.go b/cmd/run.go index 4528995a..5fd045b1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -35,17 +35,18 @@ in your own machine.`, defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) - operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, interactiveFlag, nil) + operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, interactiveFlag, nil) }, } -func captureRunFlags() *operations.RunFlags { +func captureRunFlags(assistant bool) *operations.RunFlags { return &operations.RunFlags{ AccountName: AccountName(), WorkspaceId: workspaceId, ValidityTime: validityTime, EnvironmentFile: environmentFile, RobotYaml: robotFile, + Assistant: assistant, } } diff --git a/cmd/sharedvariables.go b/cmd/sharedvariables.go index c9f70404..7699fbc9 100644 --- a/cmd/sharedvariables.go +++ b/cmd/sharedvariables.go @@ -32,5 +32,6 @@ var ( templateName string validityTime int workspaceId string + wskey string zipfile string ) diff --git a/cmd/shell.go b/cmd/shell.go index 1763acd0..b5b53b87 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -29,7 +29,7 @@ command within that environment.`, if simple { pretty.Exit(1, "Cannot do shell for simple execution model.") } - operations.ExecuteTask(captureRunFlags(), conda.Shell, config, todo, label, true, nil) + operations.ExecuteTask(captureRunFlags(false), conda.Shell, config, todo, label, true, nil) }, } diff --git a/cmd/testrun.go b/cmd/testrun.go index 69ad9469..e4b06755 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -59,7 +59,7 @@ var testrunCmd = &cobra.Command{ simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) - operations.SelectExecutionModel(captureRunFlags(), simple, todo.Commandline(), config, todo, label, false, nil) + operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, false, nil) }, } diff --git a/cmd/variables.go b/cmd/variables.go index 7f1ad438..5a700172 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -23,6 +23,9 @@ func asSimpleMap(line string) map[string]string { if len(parts) != 2 { return nil } + if len(parts[0]) == 0 { + return nil + } result := make(map[string]string) result["key"] = parts[0] result["value"] = parts[1] diff --git a/common/version.go b/common/version.go index fc815d7e..687717d8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.7` + Version = `v9.8.8` ) diff --git a/conda/validate.go b/conda/validate.go index 2fd4ccda..dfa75242 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -8,8 +8,6 @@ import ( "github.com/robocorp/rcc/settings" ) -const () - var ( longPathSupportArticle = settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") diff --git a/docs/changelog.md b/docs/changelog.md index c24c7d76..26d3c40d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # rcc change log +## v9.8.8 (date: 30.3.2021) + +- mixed fixes and experiments edition +- ignoring empty variable names on environment dumps, closes #17 +- added some missing content types to web requests +- added experimental ephemeral ECC implementation +- more common timeline markers added +- will not list pip dependencies on assistant runs +- will not ask cloud for runtime authorization (bug fix) + ## v9.8.7 (date: 26.3.2021) - more finalization of settings.yaml change diff --git a/go.mod b/go.mod index 9247637c..35519f46 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/spf13/viper v1.7.1 golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d gopkg.in/ini.v1 v1.55.0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 6d2e485c..ecb73ce1 100644 --- a/go.sum +++ b/go.sum @@ -211,6 +211,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -325,6 +326,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/operations/assistant.go b/operations/assistant.go index 2867c34b..c69b4145 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -265,6 +265,7 @@ func BeatAssistantRun(client cloud.Client, account *account, workspaceId, assist token["seq"] = beat request := client.NewRequest(fmt.Sprintf(beatAssistantApi, workspaceId, assistantId, runId)) request.Headers[authorization] = WorkspaceToken(credentials) + request.Headers[contentType] = applicationJson blob, err := json.Marshal(token) if err == nil { request.Body = bytes.NewReader(blob) @@ -299,13 +300,18 @@ func StopAssistantRun(client cloud.Client, account *account, workspaceId, assist return nil } -func StartAssistantRun(client cloud.Client, account *account, workspaceId, assistantId string) (*AssistantRobot, error) { +func StartAssistantRun(client cloud.Client, account *account, workspaceId, assistantId string, ecc bool) (*AssistantRobot, error) { common.Timeline("start assistant run: %q", assistantId) credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { return nil, err } - key, err := GenerateEphemeralKey() + var key Ephemeral + if ecc { + key, err = GenerateEphemeralEccKey() + } else { + key, err = GenerateEphemeralKey() + } if err != nil { return nil, err } diff --git a/operations/security.go b/operations/encryptionv1.go similarity index 90% rename from operations/security.go rename to operations/encryptionv1.go index 9ce06109..92c5305d 100644 --- a/operations/security.go +++ b/operations/encryptionv1.go @@ -14,8 +14,15 @@ import ( "errors" "fmt" "io" + + "github.com/robocorp/rcc/common" ) +type Ephemeral interface { + RequestBody(interface{}) (io.Reader, error) + Decode([]byte) ([]byte, error) +} + type EncryptionKeys struct { Iv string `json:"iv"` Atag string `json:"atag"` @@ -35,7 +42,9 @@ func Decoded(content string) ([]byte, error) { return base64.StdEncoding.DecodeString(content) } -func GenerateEphemeralKey() (*EncryptionV1, error) { +func GenerateEphemeralKey() (Ephemeral, error) { + common.Timeline("start ephemeral key generation") + defer common.Timeline("done ephemeral key generation") key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err @@ -43,14 +52,6 @@ func GenerateEphemeralKey() (*EncryptionV1, error) { return &EncryptionV1{key}, nil } -func (it *EncryptionV1) PublicDER() string { - public, ok := it.Public().(*rsa.PublicKey) - if !ok { - return "" - } - return base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(public)) -} - func (it *EncryptionV1) PublicPEM() string { public, ok := it.Public().(*rsa.PublicKey) if !ok { diff --git a/operations/encryptionv2.go b/operations/encryptionv2.go new file mode 100644 index 00000000..01630eb8 --- /dev/null +++ b/operations/encryptionv2.go @@ -0,0 +1,78 @@ +package operations + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "io" + + "github.com/robocorp/rcc/common" + "gopkg.in/square/go-jose.v2" +) + +type EncryptionV2 struct { + *ecdsa.PrivateKey +} + +func GenerateEphemeralEccKey() (Ephemeral, error) { + common.Timeline("start ephemeral key generation") + defer common.Timeline("done ephemeral key generation") + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, err + } + return &EncryptionV2{key}, nil +} + +func (it *EncryptionV2) PublicPEM() (string, error) { + bytes, err := x509.MarshalPKIXPublicKey(it.Public()) + if err != nil { + return "", err + } + block := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: []byte(bytes), + } + result := pem.EncodeToMemory(block) + return string(result), nil +} + +func (it *EncryptionV2) RequestObject(payload interface{}) ([]byte, error) { + result := make(Token) + encryption := make(Token) + encryption["scheme"] = "rc-encryption-v2" + envelope, err := it.PublicPEM() + if err != nil { + return nil, err + } + encryption["publicKey"] = envelope + result["encryption"] = encryption + if payload != nil { + result["payload"] = payload + } + return json.Marshal(result) +} + +func (it *EncryptionV2) RequestBody(payload interface{}) (io.Reader, error) { + blob, err := it.RequestObject(payload) + if err != nil { + return nil, err + } + return bytes.NewReader(blob), nil +} + +func (it *EncryptionV2) Decode(blob []byte) ([]byte, error) { + jwe, err := jose.ParseEncrypted(string(blob)) + if err != nil { + return nil, err + } + payload, err := jwe.Decrypt(it.PrivateKey) + if err != nil { + return nil, err + } + return payload, nil +} diff --git a/operations/running.go b/operations/running.go index 5c1dad11..fba1ae03 100644 --- a/operations/running.go +++ b/operations/running.go @@ -24,6 +24,7 @@ type RunFlags struct { ValidityTime int EnvironmentFile string RobotYaml string + Assistant bool } func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { @@ -73,8 +74,8 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. } func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { - common.Timeline("execution starts.") - defer common.Timeline("execution done.") + common.Timeline("robot execution starts (simple=%v).", simple) + defer common.Timeline("robot execution done.") if simple { ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { @@ -150,7 +151,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro pretty.Exit(7, "Error: %v", err) } var data Token - if len(flags.WorkspaceId) > 0 { + if !flags.Assistant && len(flags.WorkspaceId) > 0 { claims := RunClaims(flags.ValidityTime*60, flags.WorkspaceId) data, err = AuthorizeClaims(flags.AccountName, claims) } @@ -179,7 +180,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro before := make(map[string]string) beforeHash, beforeErr := conda.DigestFor(label, before) outputDir := todo.ArtifactDirectory(config) - if !common.Silent && !interactive { + if !flags.Assistant && !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } common.Debug("DEBUG: about to run command - %v", task) diff --git a/operations/security_test.go b/operations/security_test.go index b2ad5054..91c6c3bd 100644 --- a/operations/security_test.go +++ b/operations/security_test.go @@ -1,7 +1,9 @@ package operations_test import ( + "crypto/ecdsa" "crypto/rsa" + "fmt" "strings" "testing" @@ -9,17 +11,49 @@ import ( "github.com/robocorp/rcc/operations" ) -func TestCanCreatePrivateKey(t *testing.T) { +func TestCanCreatePrivateEccKey(t *testing.T) { must, wont := hamlet.Specifications(t) - key, err := operations.GenerateEphemeralKey() + ephemeral, err := operations.GenerateEphemeralEccKey() must.Nil(err) + wont.Nil(ephemeral) + key, ok := ephemeral.(*operations.EncryptionV2) + must.True(ok) + wont.Nil(key) + wont.Nil(key.Public()) + publicKey, ok := key.Public().(*ecdsa.PublicKey) + must.True(ok) + wont.Nil(publicKey) + envelope, err := key.PublicPEM() + fmt.Println(envelope) + must.Nil(err) + must.Equal(215, len(envelope)) + body, err := key.RequestObject(nil) + must.Nil(err) + must.Equal(279, len(body)) + textual := string(body) + must.True(strings.Contains(textual, "encryption")) + must.True(strings.Contains(textual, "scheme")) + must.True(strings.Contains(textual, "publicKey")) + reader, err := key.RequestBody("hello, world!") + must.Nil(err) + wont.Nil(reader) +} + +func TestCanCreatePrivateRsaKey(t *testing.T) { + must, wont := hamlet.Specifications(t) + + ephemeral, err := operations.GenerateEphemeralKey() + must.Nil(err) + wont.Nil(ephemeral) + key, ok := ephemeral.(*operations.EncryptionV1) + must.True(ok) wont.Nil(key) wont.Nil(key.Public()) publicKey, ok := key.Public().(*rsa.PublicKey) must.True(ok) + wont.Nil(publicKey) must.Equal(256, publicKey.Size()) - must.Equal(360, len(key.PublicDER())) must.Equal(426, len(key.PublicPEM())) body, err := key.RequestObject(nil) must.Nil(err) diff --git a/operations/workspaces.go b/operations/workspaces.go index 49bde106..f63be7f7 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -144,6 +144,7 @@ func NewRobotCommand(client cloud.Client, account *account, workspace, robotName } request := client.NewRequest(fmt.Sprintf(newRobotApi, workspace)) request.Headers[authorization] = BearerToken(credentials) + request.Headers[contentType] = applicationJson request.Body = strings.NewReader(body) response := client.Post(request) if response.Status != 200 { diff --git a/shell/task.go b/shell/task.go index 307ffecf..4fdd9eca 100644 --- a/shell/task.go +++ b/shell/task.go @@ -53,6 +53,7 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) if err != nil { return -500, err } + common.Timeline("exec %q started", it.executable) common.Debug("PID #%d is %q.", command.Process.Pid, command) defer func() { common.Debug("PID #%d finished: %v.", command.Process.Pid, command.ProcessState) From 9c41fb0bc8da8bde3066ba8796b541949bcef484 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 29 Mar 2021 18:11:50 +0300 Subject: [PATCH 099/516] FIX: added cloud-ui to settings (v9.8.9) - added `cloud-ui` to settings.yaml --- assets/settings.yaml | 1 + common/version.go | 2 +- docs/changelog.md | 6 +++++- settings/data.go | 3 +++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index fd567ebd..ab286f57 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,6 +1,7 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://id.robocorp.com/ + cloud-ui: https://cloud.robocorp.com/ pypi: # https://pypi.org/simple/ pypi-trusted: # https://pypi.org/ conda: # https://repo.anaconda.org/ diff --git a/common/version.go b/common/version.go index 687717d8..48bf83b4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.8` + Version = `v9.8.9` ) diff --git a/docs/changelog.md b/docs/changelog.md index 26d3c40d..a0f42670 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v9.8.8 (date: 30.3.2021) +## v9.8.9 (date: 29.3.2021) + +- added `cloud-ui` to settings.yaml + +## v9.8.8 (date: 29.3.2021) - mixed fixes and experiments edition - ignoring empty variable names on environment dumps, closes #17 diff --git a/settings/data.go b/settings/data.go index b3dc2e8d..49d8fab0 100644 --- a/settings/data.go +++ b/settings/data.go @@ -122,6 +122,7 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { } else { correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) correct = diagnoseUrl(it.Endpoints.CloudLinking, "endpoints/cloud-linking", diagnose, correct) + correct = diagnoseUrl(it.Endpoints.CloudUi, "endpoints/cloud-ui", diagnose, correct) correct = diagnoseUrl(it.Endpoints.Docs, "endpoints/docs", diagnose, correct) correct = diagnoseUrl(it.Endpoints.Issues, "endpoints/issues", diagnose, correct) correct = diagnoseUrl(it.Endpoints.Telemetry, "endpoints/telemetry", diagnose, correct) @@ -152,6 +153,7 @@ type Certificates struct { type Endpoints struct { CloudApi string `yaml:"cloud-api" json:"cloud-api"` CloudLinking string `yaml:"cloud-linking" json:"cloud-linking"` + CloudUi string `yaml:"cloud-ui" json:"cloud-ui"` Conda string `yaml:"conda" json:"conda"` Docs string `yaml:"docs" json:"docs"` Downloads string `yaml:"downloads" json:"downloads"` @@ -184,6 +186,7 @@ func (it *Endpoints) Hostnames() []string { collector := make(map[string]bool) hostFromUrl(it.CloudApi, collector) hostFromUrl(it.CloudLinking, collector) + hostFromUrl(it.CloudUi, collector) hostFromUrl(it.Conda, collector) hostFromUrl(it.Docs, collector) hostFromUrl(it.Downloads, collector) From d6682057a1c9186059ae283398b61dbac81cdb18 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 30 Mar 2021 09:14:04 +0300 Subject: [PATCH 100/516] FIX: panic from settings.yaml (v9.8.10) - fix: no more panics when directly writing to settings.yaml --- common/version.go | 2 +- conda/platform_windows_amd64.go | 1 + conda/validate.go | 4 +--- docs/changelog.md | 4 ++++ operations/diagnostics.go | 12 ++++++------ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 48bf83b4..60f5cb1e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.9` + Version = `v9.8.10` ) diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 07b9c8b3..2ec10aee 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -81,6 +81,7 @@ func HasLongPathSupport() bool { code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).Transparent() common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) if err != nil { + longPathSupportArticle := settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") common.Log("%sWARNING! Long path support failed. Reason: %v.%s", pretty.Red, err, pretty.Reset) common.Log("%sWARNING! See %v for more details.%s", pretty.Red, longPathSupportArticle, pretty.Reset) return false diff --git a/conda/validate.go b/conda/validate.go index dfa75242..a1689e6e 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -5,12 +5,10 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/settings" ) var ( - longPathSupportArticle = settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") - validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") + validPathCharacters = regexp.MustCompile("(?i)^[.a-z0-9_:/\\\\~-]+$") ) func ValidLocation(value string) bool { diff --git a/docs/changelog.md b/docs/changelog.md index a0f42670..2e6b06f9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.8.10 (date: 30.3.2021) + +- fix: no more panics when directly writing to settings.yaml + ## v9.8.9 (date: 29.3.2021) - added `cloud-ui` to settings.yaml diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 2d6d995c..fff18fd0 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -33,12 +33,6 @@ const ( statusFatal = `fatal` ) -var ( - supportLongPathUrl = settings.Global.DocsLink("troubleshooting/windows-long-path") - supportNetworkUrl = settings.Global.DocsLink("troubleshooting/firewall-and-proxies") - supportGeneralUrl = settings.Global.DocsLink("troubleshooting") -) - type stringerr func() (string, error) func justText(source stringerr) string { @@ -95,6 +89,7 @@ func rccStatusLine() string { } func longPathSupportCheck() *common.DiagnosticCheck { + supportLongPathUrl := settings.Global.DocsLink("troubleshooting/windows-long-path") if conda.HasLongPathSupport() { return &common.DiagnosticCheck{ Type: "OS", @@ -112,6 +107,7 @@ func longPathSupportCheck() *common.DiagnosticCheck { } func robocorpHomeCheck() *common.DiagnosticCheck { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { return &common.DiagnosticCheck{ Type: "RPA", @@ -129,6 +125,7 @@ func robocorpHomeCheck() *common.DiagnosticCheck { } func dnsLookupCheck(site string) *common.DiagnosticCheck { + supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") found, err := net.LookupHost(site) if err != nil { return &common.DiagnosticCheck{ @@ -147,6 +144,7 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { } func canaryDownloadCheck() *common.DiagnosticCheck { + supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") client, err := cloud.NewClient(settings.Global.DownloadsLink("")) if err != nil { return &common.DiagnosticCheck{ @@ -233,6 +231,7 @@ func ProduceDiagnostics(filename, robotfile string, json bool) (*common.Diagnost type Unmarshaler func([]byte, interface{}) error func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []string, target *common.DiagnosticStatus) { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") target.Details[fmt.Sprintf("%s-file-count", strings.ToLower(label))] = fmt.Sprintf("%d file(s)", len(paths)) diagnose := target.Diagnose(label) var canary interface{} @@ -267,6 +266,7 @@ func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { } func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus) { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") config, err := robot.LoadRobotYaml(robotfile, false) diagnose := target.Diagnose("Robot") if err != nil { From 56453c69575db0a6091c59d6e46d173fbbe28cf3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 30 Mar 2021 18:00:37 +0300 Subject: [PATCH 101/516] FIX: fix reported bugs (v9.8.11) - added Accept header to micromamba download command - made some URL diagnostics optional, if they are left empty --- common/version.go | 2 +- conda/download.go | 7 ++++++- docs/changelog.md | 5 +++++ settings/data.go | 31 +++++++++++++++++-------------- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/common/version.go b/common/version.go index 60f5cb1e..9c72817f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.10` + Version = `v9.8.11` ) diff --git a/conda/download.go b/conda/download.go index c5cd8adb..fef4a51f 100644 --- a/conda/download.go +++ b/conda/download.go @@ -18,7 +18,12 @@ func DownloadMicromamba() error { url := MicromambaLink() filename := BinMicromamba() client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} - response, err := client.Get(url) + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + request.Header.Add("Accept", "application/octet-stream") + response, err := client.Do(request) if err != nil { return err } diff --git a/docs/changelog.md b/docs/changelog.md index 2e6b06f9..b87e690b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.8.11 (date: 30.3.2021) + +- added Accept header to micromamba download command +- made some URL diagnostics optional, if they are left empty + ## v9.8.10 (date: 30.3.2021) - fix: no more panics when directly writing to settings.yaml diff --git a/settings/data.go b/settings/data.go index 49d8fab0..892cc22d 100644 --- a/settings/data.go +++ b/settings/data.go @@ -94,6 +94,14 @@ func diagnoseUrl(link, label string, diagnose common.Diagnoser, correct bool) bo return correct } +func diagnoseOptionalUrl(link, label string, diagnose common.Diagnoser, correct bool) bool { + if len(strings.TrimSpace(link)) == 0 { + return correct + } else { + return diagnoseUrl(link, label, diagnose, correct) + } +} + func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStatus) { diagnose := target.Diagnose("settings.yaml") correct := true @@ -121,21 +129,16 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { correct = false } else { correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.CloudLinking, "endpoints/cloud-linking", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.CloudUi, "endpoints/cloud-ui", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Docs, "endpoints/docs", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Issues, "endpoints/issues", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Telemetry, "endpoints/telemetry", diagnose, correct) correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) - if len(it.Endpoints.Conda) > 0 { - correct = diagnoseUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) - } - if len(it.Endpoints.Pypi) > 0 { - correct = diagnoseUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) - } - if len(it.Endpoints.PypiTrusted) > 0 { - correct = diagnoseUrl(it.Endpoints.PypiTrusted, "endpoints/pypi-trusted", diagnose, correct) - } + + correct = diagnoseOptionalUrl(it.Endpoints.CloudUi, "endpoints/cloud-ui", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.CloudLinking, "endpoints/cloud-linking", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.Issues, "endpoints/issues", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.Telemetry, "endpoints/telemetry", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.Docs, "endpoints/docs", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints.PypiTrusted, "endpoints/pypi-trusted", diagnose, correct) } if it.Meta == nil { diagnose.Warning("", "settings.yaml: meta section is totally missing") From 632196fec21ff95650728f6a6a6b5ab4657f7852 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 31 Mar 2021 13:53:41 +0300 Subject: [PATCH 102/516] RCC-139: initial holotree integration (v9.9.0) - added holotree as part of source code (but not as integrated part yet) - added new internal command: holotree --- anywork/worker.go | 70 ++++++++++++ cmd/holotree.go | 111 ++++++++++++++++++ common/elapsed.go | 6 + common/version.go | 2 +- conda/workflows.go | 4 +- docs/changelog.md | 5 + go.mod | 1 + go.sum | 2 + htfs/directory.go | 241 +++++++++++++++++++++++++++++++++++++++ htfs/fs_test.go | 43 +++++++ htfs/functions.go | 221 +++++++++++++++++++++++++++++++++++ htfs/library.go | 173 ++++++++++++++++++++++++++++ pathlib/functions.go | 8 ++ trollhash/algorithm.go | 175 ++++++++++++++++++++++++++++ trollhash/rrhash_test.go | 60 ++++++++++ 15 files changed, 1119 insertions(+), 3 deletions(-) create mode 100644 anywork/worker.go create mode 100644 cmd/holotree.go create mode 100644 htfs/directory.go create mode 100644 htfs/fs_test.go create mode 100644 htfs/functions.go create mode 100644 htfs/library.go create mode 100644 trollhash/algorithm.go create mode 100644 trollhash/rrhash_test.go diff --git a/anywork/worker.go b/anywork/worker.go new file mode 100644 index 00000000..8c8d186a --- /dev/null +++ b/anywork/worker.go @@ -0,0 +1,70 @@ +package anywork + +import ( + "fmt" + "os" + "sync" +) + +var ( + group *sync.WaitGroup + pipeline WorkQueue + headcount uint64 +) + +type Work func() +type WorkQueue chan Work + +func catcher(title string, identity uint64) { + catch := recover() + if catch != nil { + fmt.Fprintf(os.Stderr, "Recovering %q #%d: %v\n", title, identity, catch) + } +} + +func process(fun Work, identity uint64) { + defer group.Done() + defer catcher("process", identity) + fun() +} + +func member(identity uint64) { + defer catcher("member", identity) + for { + work, ok := <-pipeline + if !ok { + break + } + process(work, identity) + } +} + +func init() { + group = &sync.WaitGroup{} + pipeline = make(WorkQueue, 100000) + headcount = 1 + go member(headcount) +} + +func Scale(limit uint64) { + for headcount < limit { + go member(headcount) + headcount += 1 + } +} + +func Backlog(todo Work) { + if todo != nil { + group.Add(1) + pipeline <- todo + } +} + +func Sync() { + group.Wait() +} + +func Done() { + close(pipeline) + group.Wait() +} diff --git a/cmd/holotree.go b/cmd/holotree.go new file mode 100644 index 00000000..16875f39 --- /dev/null +++ b/cmd/holotree.go @@ -0,0 +1,111 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime/pprof" + + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + holotreeBlueprint []byte + holotreeProfiled string + holotreeSpace string +) + +var holotreeCmd = &cobra.Command{ + Use: "holotree conda.yaml+", + Aliases: []string{"htfs"}, + Short: "Do holotree operations.", + Long: "Do holotree operations.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree command lasted").Report() + } + + var left, right *conda.Environment + var err error + + for _, filename := range args { + left = right + right, err = conda.ReadCondaYaml(filename) + pretty.Guard(err == nil, 1, "Failure: %v", err) + if left == nil { + continue + } + right, err = left.Merge(right) + pretty.Guard(err == nil, 1, "Failure: %v", err) + } + pretty.Guard(right != nil, 1, "Missing environment specification(s).") + content, err := right.AsYaml() + pretty.Guard(err == nil, 1, "YAML error: %v", err) + holotreeBlueprint = []byte(content) + + ok := conda.MustMicromamba() + pretty.Guard(ok, 1, "Could not get micromamba installed.") + + tree, err := htfs.New(common.RobocorpHome()) + pretty.Guard(err == nil, 2, "Failed to create holotree location, reason %v.", err) + + // following must be setup here + common.StageFolder = tree.Stage() + common.Stageonly = true + common.Liveonly = true + + err = os.RemoveAll(tree.Stage()) + pretty.Guard(err == nil, 3, "Failed to clean stage, reason %v.", err) + + common.Debug("Holotree stage is %q.", tree.Stage()) + exists := tree.HasBlueprint(holotreeBlueprint) + common.Debug("Has blueprint environment: %v", exists) + + if !exists { + identityfile := filepath.Join(tree.Stage(), "identity.yaml") + err = ioutil.WriteFile(identityfile, holotreeBlueprint, 0o640) + pretty.Guard(err == nil, 3, "Failed to save %q, reason %v.", identityfile, err) + label, err := conda.NewEnvironment(false, identityfile) + pretty.Guard(err == nil, 3, "Failed to create environment, reason %v.", err) + common.Debug("Label: %q", label) + } + + anywork.Scale(17) + + profiling := false + if holotreeProfiled != "" { + sink, err := os.Create(holotreeProfiled) + pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", holotreeProfiled, err) + defer sink.Close() + err = pprof.StartCPUProfile(sink) + pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) + profiling = true + } + + if !exists { + err := tree.Record(holotreeBlueprint) + pretty.Guard(err == nil, 7, "Failed to record blueprint %q, reason: %v", string(holotreeBlueprint), err) + } + + path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(holotreeSpace)) + pretty.Guard(err == nil, 10, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + if profiling { + pprof.StopCPUProfile() + } + fmt.Fprintln(os.Stdout, path) + }, +} + +func init() { + internalCmd.AddCommand(holotreeCmd) + holotreeCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") + holotreeCmd.MarkFlagRequired("space") + holotreeCmd.Flags().StringVar(&holotreeProfiled, "profile", "", "Filename to save profiling information.") +} diff --git a/common/elapsed.go b/common/elapsed.go index a14f3310..63ebaad3 100644 --- a/common/elapsed.go +++ b/common/elapsed.go @@ -41,6 +41,12 @@ func (it *stopwatch) Elapsed() Duration { return Duration(time.Since(it.started)) } +func (it *stopwatch) Debug() Duration { + elapsed := it.Elapsed() + Debug("%v %v", it.message, elapsed) + return elapsed +} + func (it *stopwatch) Log() Duration { elapsed := it.Elapsed() Log("%v %v", it.message, elapsed) diff --git a/common/version.go b/common/version.go index 9c72817f..443dd52e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.8.11` + Version = `v9.9.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 451e23f4..968b9075 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -303,7 +303,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. if err != nil { return "", "", nil, err } - hash := shortDigest(yaml) + hash := ShortDigest(yaml) if !save { return hash, yaml, right, nil } @@ -317,7 +317,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. return hash, yaml, right, err } -func shortDigest(content string) string { +func ShortDigest(content string) string { digester := sha256.New() digester.Write([]byte(content)) result := Hexdigest(digester.Sum(nil)) diff --git a/docs/changelog.md b/docs/changelog.md index b87e690b..ddd1e7ec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.0 (date: 31.3.2021) + +- added holotree as part of source code (but not as integrated part yet) +- added new internal command: holotree + ## v9.8.11 (date: 30.3.2021) - added Accept header to micromamba download command diff --git a/go.mod b/go.mod index 35519f46..9539f306 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/robocorp/rcc go 1.14 require ( + github.com/dchest/siphash v1.2.2 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 diff --git a/go.sum b/go.sum index ecb73ce1..761a7041 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= +github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= diff --git a/htfs/directory.go b/htfs/directory.go new file mode 100644 index 00000000..869588b7 --- /dev/null +++ b/htfs/directory.go @@ -0,0 +1,241 @@ +package htfs + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/pathlib" +) + +var ( + killfile map[string]bool +) + +func init() { + killfile = make(map[string]bool) + killfile["__pycache__"] = true + killfile[".pyc"] = true + killfile[".git"] = true + killfile[".hg"] = true + killfile[".svn"] = true + killfile[".gitignore"] = true +} + +type Filetask func(string, *File) anywork.Work +type Dirtask func(string, *Dir) anywork.Work +type Treetop func(string, *Dir) error + +type Root struct { + Identity string `json:"identity"` + Path string `json:"path"` + Lifted bool `json:"lifted"` + Tree *Dir `json:"tree"` +} + +func NewRoot(path string) (*Root, error) { + fullpath, err := pathlib.Abs(path) + if err != nil { + return nil, err + } + basename := filepath.Base(fullpath) + return &Root{ + Identity: basename, + Path: fullpath, + Lifted: false, + Tree: newDir(""), + }, nil +} + +func (it *Root) Rewrite() []byte { + return []byte(it.Identity) +} + +func (it *Root) Relocate(target string) error { + origin := filepath.Dir(it.Path) + locate := filepath.Dir(target) + if origin != locate { + return fmt.Errorf("Base directory mismatch: %q vs %q.", origin, locate) + } + basename := filepath.Base(target) + if len(it.Identity) != len(basename) { + return fmt.Errorf("Base name length mismatch: %q vs %q.", it.Identity, basename) + } + if len(it.Path) != len(target) { + return fmt.Errorf("Path length mismatch: %q vs %q.", it.Path, target) + } + it.Path = target + it.Identity = basename + return nil +} + +func (it *Root) Lift() error { + if it.Lifted { + return nil + } + it.Lifted = true + return it.Tree.Lift(it.Path) +} + +func (it *Root) Treetop(task Treetop) error { + err := task(it.Path, it.Tree) + anywork.Sync() + return err +} + +func (it *Root) AllDirs(task Dirtask) { + it.Tree.AllDirs(it.Path, task) + anywork.Sync() +} + +func (it *Root) AllFiles(task Filetask) { + it.Tree.AllFiles(it.Path, task) + anywork.Sync() +} + +func (it *Root) AsJson() ([]byte, error) { + return json.MarshalIndent(it, "", " ") +} + +func (it *Root) SaveAs(filename string) error { + content, err := it.AsJson() + if err != nil { + return err + } + sink, err := os.Create(filename) + if err != nil { + return err + } + defer sink.Close() + defer sink.Sync() + writer, err := gzip.NewWriterLevel(sink, gzip.BestSpeed) + if err != nil { + return err + } + defer writer.Close() + _, err = writer.Write(content) + if err != nil { + return err + } + return nil +} + +func (it *Root) LoadFrom(filename string) error { + source, err := os.Open(filename) + if err != nil { + return err + } + defer source.Close() + reader, err := gzip.NewReader(source) + if err != nil { + return err + } + defer reader.Close() + content := bytes.NewBuffer(nil) + io.Copy(content, reader) + return json.Unmarshal(content.Bytes(), &it) +} + +type Dir struct { + Name string `json:"name"` + Mode fs.FileMode `json:"mode"` + Dirs map[string]*Dir `json:"subdirs"` + Files map[string]*File `json:"files"` +} + +func (it *Dir) AllDirs(path string, task Dirtask) { + for name, dir := range it.Dirs { + fullpath := filepath.Join(path, name) + dir.AllDirs(fullpath, task) + } + anywork.Backlog(task(path, it)) +} + +func (it *Dir) AllFiles(path string, task Filetask) { + for name, dir := range it.Dirs { + fullpath := filepath.Join(path, name) + dir.AllFiles(fullpath, task) + } + for name, file := range it.Files { + fullpath := filepath.Join(path, name) + anywork.Backlog(task(fullpath, file)) + } +} + +func (it *Dir) Lift(path string) error { + stat, err := os.Stat(path) + if err != nil { + return err + } + it.Mode = stat.Mode() + source, err := os.Open(path) + if err != nil { + return err + } + defer source.Close() + content, err := source.ReadDir(-1) + if err != nil { + return err + } + for _, part := range content { + if killfile[part.Name()] || killfile[filepath.Ext(part.Name())] { + continue + } + // following must be done to get by symbolic links + info, err := os.Stat(filepath.Join(path, part.Name())) + if err != nil { + return err + } + if info.IsDir() { + it.Dirs[part.Name()] = newDir(info.Name()) + continue + } + it.Files[part.Name()] = newFile(info) + } + for name, dir := range it.Dirs { + err = dir.Lift(filepath.Join(path, name)) + if err != nil { + return err + } + } + return nil +} + +type File struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode fs.FileMode `json:"mode"` + Digest string `json:"digest"` + Rewrite []int64 `json:"rewrite"` +} + +func (it *File) Match(info fs.FileInfo) bool { + name := it.Name == info.Name() + size := it.Size == info.Size() + mode := it.Mode == info.Mode() + return name && size && mode +} + +func newDir(name string) *Dir { + return &Dir{ + Name: name, + Dirs: make(map[string]*Dir), + Files: make(map[string]*File), + } +} + +func newFile(info fs.FileInfo) *File { + return &File{ + Name: info.Name(), + Mode: info.Mode(), + Size: info.Size(), + Digest: "N/A", + Rewrite: make([]int64, 0), + } +} diff --git a/htfs/fs_test.go b/htfs/fs_test.go new file mode 100644 index 00000000..eeedec9d --- /dev/null +++ b/htfs/fs_test.go @@ -0,0 +1,43 @@ +package htfs_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/htfs" +) + +func TestHTFSspecification(t *testing.T) { + must, wont := hamlet.Specifications(t) + + filename := filepath.Join(os.TempDir(), "htfs_test.json") + + fs, err := htfs.NewRoot("..") + must.Nil(err) + wont.Nil(fs) + wont.Nil(fs.Tree) + + must.Nil(fs.Lift()) + + content, err := fs.AsJson() + must.Nil(err) + must.True(len(content) > 50000) + + must.Nil(fs.SaveAs(filename)) + + reloaded, err := htfs.NewRoot(".") + must.Nil(err) + wont.Nil(reloaded) + before, err := reloaded.AsJson() + must.Nil(err) + must.True(len(before) < 200) + wont.Equal(fs.Path, reloaded.Path) + + must.Nil(reloaded.LoadFrom(filename)) + after, err := reloaded.AsJson() + must.Nil(err) + must.Equal(len(after), len(content)) + must.Equal(fs.Path, reloaded.Path) +} diff --git a/htfs/functions.go b/htfs/functions.go new file mode 100644 index 00000000..d61ea5c5 --- /dev/null +++ b/htfs/functions.go @@ -0,0 +1,221 @@ +package htfs + +import ( + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/trollhash" +) + +func Locator(seek string) Filetask { + return func(fullpath string, details *File) anywork.Work { + return func() { + source, err := os.Open(fullpath) + if err != nil { + panic(fmt.Sprintf("Open %q, reason: %v", fullpath, err)) + } + defer source.Close() + digest := sha256.New() + locator := trollhash.LocateWriter(digest, seek) + _, err = io.Copy(locator, source) + if err != nil { + panic(fmt.Sprintf("Copy %q, reason: %v", fullpath, err)) + } + details.Rewrite = locator.Locations() + details.Digest = fmt.Sprintf("%02x", digest.Sum(nil)) + } + } +} + +func MakeBranches(path string, it *Dir) error { + for _, subdir := range it.Dirs { + err := MakeBranches(filepath.Join(path, subdir.Name), subdir) + if err != nil { + return err + } + } + if len(it.Dirs) == 0 { + err := os.MkdirAll(path, 0o750) + if err != nil { + return err + } + } + return os.Chtimes(path, motherTime, motherTime) +} + +func ScheduleLifters(library Library, stats *stats) Treetop { + var scheduler Treetop + scheduler = func(path string, it *Dir) error { + for name, subdir := range it.Dirs { + scheduler(filepath.Join(path, name), subdir) + } + for name, file := range it.Files { + directory := library.Location(file.Digest) + if !pathlib.IsDir(directory) { + os.MkdirAll(directory, 0o755) + } + sinkpath := filepath.Join(directory, file.Digest) + ok := pathlib.IsFile(sinkpath) + stats.Dirty(!ok) + if ok { + continue + } + sourcepath := filepath.Join(path, name) + anywork.Backlog(LiftFile(sourcepath, sinkpath)) + } + return nil + } + return scheduler +} + +func LiftFile(sourcename, sinkname string) anywork.Work { + return func() { + source, err := os.Open(sourcename) + if err != nil { + panic(err) + } + defer source.Close() + sink, err := os.Create(sinkname) + if err != nil { + panic(err) + } + defer sink.Close() + writer, err := gzip.NewWriterLevel(sink, gzip.BestSpeed) + if err != nil { + panic(err) + } + _, err = io.Copy(writer, source) + if err != nil { + panic(err) + } + err = writer.Close() + if err != nil { + panic(err) + } + sink.Sync() + } +} + +func DropFile(sourcename, sinkname string, details *File, rewrite []byte) anywork.Work { + return func() { + source, err := os.Open(sourcename) + if err != nil { + panic(err) + } + defer source.Close() + reader, err := gzip.NewReader(source) + if err != nil { + panic(err) + } + defer reader.Close() + sink, err := os.Create(sinkname) + if err != nil { + panic(err) + } + defer sink.Close() + _, err = io.Copy(sink, reader) + if err != nil { + panic(err) + } + sink.Sync() + for _, position := range details.Rewrite { + _, err = sink.Seek(position, 0) + if err != nil { + panic(fmt.Sprintf("%v %d", err, position)) + } + _, err = sink.Write(rewrite) + if err != nil { + panic(err) + } + } + sink.Sync() + os.Chmod(sinkname, details.Mode) + os.Chtimes(sinkname, motherTime, motherTime) + } +} + +func RemoveFile(filename string) anywork.Work { + return func() { + err := os.Remove(filename) + if err != nil { + panic(err) + } + } +} + +func RemoveDirectory(dirname string) anywork.Work { + return func() { + err := os.RemoveAll(dirname) + if err != nil { + panic(err) + } + } +} + +func RestoreDirectory(library Library, fs *Root, stats *stats) Dirtask { + return func(path string, it *Dir) anywork.Work { + return func() { + source, err := os.Open(path) + if err != nil { + panic(err) + } + content, err := source.ReadDir(-1) + source.Close() + if err != nil { + panic(err) + } + dirs := make(map[string]bool) + files := make(map[string]bool) + for _, part := range content { + directpath := filepath.Join(path, part.Name()) + info, err := os.Stat(directpath) + if err != nil { + panic(err) + } + if info.IsDir() { + dirs[part.Name()] = true + _, ok := it.Dirs[part.Name()] + stats.Dirty(!ok) + if !ok { + anywork.Backlog(RemoveDirectory(directpath)) + //fmt.Println("Extra dir, remove:", directpath) + } + continue + } + files[part.Name()] = true + found, ok := it.Files[part.Name()] + if !ok { + stats.Dirty(true) + anywork.Backlog(RemoveFile(directpath)) + //fmt.Println("Extra file, remove:", directpath) + continue + } + ok = found.Match(info) + stats.Dirty(!ok) + if !ok { + directory := library.Location(found.Digest) + droppath := filepath.Join(directory, found.Digest) + anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) + //fmt.Println("Corrupted file, restore:", directpath) + } + } + for name, found := range it.Files { + directpath := filepath.Join(path, name) + _, seen := files[name] + if !seen { + stats.Dirty(true) + directory := library.Location(found.Digest) + droppath := filepath.Join(directory, found.Digest) + anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) + //fmt.Println("Missing file, restore:", directpath) + } + } + } + } +} diff --git a/htfs/library.go b/htfs/library.go new file mode 100644 index 00000000..8f9ecb84 --- /dev/null +++ b/htfs/library.go @@ -0,0 +1,173 @@ +package htfs + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/dchest/siphash" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" +) + +const ( + epoc = 1610000000 +) + +var ( + motherTime = time.Unix(epoc, 0) +) + +type stats struct { + sync.Mutex + total uint64 + dirty uint64 +} + +func (it *stats) Dirty(dirty bool) { + it.Lock() + defer it.Unlock() + + it.total++ + if dirty { + it.dirty++ + } +} + +type Library interface { + Identity() string + Stage() string + Record([]byte) error + Restore([]byte, []byte, []byte) (string, error) + Location(string) string + HasBlueprint([]byte) bool +} + +type hololib struct { + identity uint64 + basedir string +} + +func (it *hololib) Location(digest string) string { + return filepath.Join(it.basedir, "hololib", "library", digest[:2], digest[2:4], digest[4:6]) +} + +func (it *hololib) Identity() string { + return fmt.Sprintf("h%016xt", it.identity) +} + +func (it *hololib) Stage() string { + stage := filepath.Join(it.basedir, "holotree", it.Identity()) + err := os.MkdirAll(stage, 0o755) + if err != nil { + panic(err) + } + return stage +} + +func (it *hololib) Record(blueprint []byte) error { + defer common.Stopwatch("Holotree recording took:").Debug() + key := textual(sipit(blueprint), 0) + fs, err := NewRoot(it.Stage()) + if err != nil { + return err + } + err = fs.Lift() + if err != nil { + return err + } + fs.AllFiles(Locator(it.Identity())) + err = fs.SaveAs(filepath.Join(it.basedir, "hololib", "catalog", key)) + if err != nil { + return err + } + score := &stats{} + err = fs.Treetop(ScheduleLifters(it, score)) + common.Debug("Holotree new workload: %d/%d\n", score.dirty, score.total) + if err != nil { + return err + } + return os.RemoveAll(it.Stage()) +} + +func (it *hololib) CatalogPath(key string) string { + return filepath.Join(it.basedir, "hololib", "catalog", key) +} + +func (it *hololib) HasBlueprint(blueprint []byte) bool { + key := textual(sipit(blueprint), 0) + return pathlib.IsFile(it.CatalogPath(key)) +} + +func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { + defer common.Stopwatch("Holotree restore took:").Debug() + key := textual(sipit(blueprint), 0) + prefix := textual(sipit(client), 9) + suffix := textual(sipit(tag), 8) + name := prefix + "_" + suffix + fs, err := NewRoot(it.Stage()) + if err != nil { + return "", err + } + err = fs.LoadFrom(filepath.Join(it.basedir, "hololib", "catalog", key)) + if err != nil { + return "", err + } + where := filepath.Join(it.basedir, "holotree", name) + err = fs.Relocate(where) + if err != nil { + return "", err + } + err = fs.Treetop(MakeBranches) + if err != nil { + return "", err + } + score := &stats{} + fs.AllDirs(RestoreDirectory(it, fs, score)) + common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + return where, nil +} + +func sipit(key []byte) uint64 { + return siphash.Hash(9007199254740993, 2147483647, key) +} + +func textual(key uint64, size int) string { + text := fmt.Sprintf("%016x", key) + if size > 0 { + return text[:size] + } + return text +} + +func makedirs(prefix string, suffixes ...string) error { + for _, suffix := range suffixes { + fullpath := filepath.Join(prefix, suffix) + err := os.MkdirAll(fullpath, 0o755) + if err != nil { + return err + } + } + return nil +} + +func New(location string) (Library, error) { + basedir, err := filepath.Abs(location) + if err != nil { + return nil, err + } + err = makedirs(basedir, "hololib", "holotree") + if err != nil { + return nil, err + } + err = makedirs(filepath.Join(basedir, "hololib"), "library", "catalog") + if err != nil { + return nil, err + } + return &hololib{ + identity: sipit([]byte(basedir)), + basedir: basedir, + }, nil +} diff --git a/pathlib/functions.go b/pathlib/functions.go index ea531b5b..4e213432 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -12,6 +12,14 @@ func Exists(pathname string) bool { return !os.IsNotExist(err) } +func Abs(path string) (string, error) { + fullpath, err := filepath.Abs(path) + if err != nil { + return "", err + } + return filepath.Clean(fullpath), nil +} + func IsDir(pathname string) bool { stat, err := os.Stat(pathname) if err != nil { diff --git a/trollhash/algorithm.go b/trollhash/algorithm.go new file mode 100644 index 00000000..da4579d0 --- /dev/null +++ b/trollhash/algorithm.go @@ -0,0 +1,175 @@ +package trollhash + +import ( + "io" +) + +type Trollhash func(byte) uint64 + +func New(size int) Trollhash { + lshift := (3 * size) % 64 + rshift := 64 - lshift + history := make([]uint64, size) + slot := 0 + troll := uint64(0) + return func(key byte) uint64 { + remove := history[slot] + tool := seedlings[key] + history[slot] = tool + troll = (troll << 3) | (troll >> 61) ^ tool + troll ^= (remove << lshift) | (remove >> rshift) + slot += 1 + if slot == size { + slot = 0 + } + return troll + } +} + +func Hash(needle []byte) (result uint64) { + hasher := New(len(needle)) + for _, add := range needle { + result = hasher(add) + } + return result +} + +type WriteLocator interface { + io.Writer + Locations() []int64 +} + +type writer struct { + delegate io.Writer + seek Seeker + found []int64 +} + +func (it *writer) Write(payload []byte) (int, error) { + for _, value := range payload { + ok, head := it.seek(value) + if ok { + it.found = append(it.found, head) + } + } + return it.delegate.Write(payload) +} + +func (it *writer) Locations() []int64 { + return it.found +} + +func LocateWriter(delegate io.Writer, needle string) WriteLocator { + result := &writer{ + delegate: delegate, + seek: Find(needle), + found: make([]int64, 0, 20), + } + return result +} + +type Seeker func(byte) (bool, int64) + +func makeSeeker(verify []byte) Seeker { + goal := Hash(verify) + limit := int64(len(verify)) + cursor, window := int64(-1), make([]byte, limit) + hasher := New(int(limit)) + slot, size := 0, int(limit) + return func(add byte) (bool, int64) { + cursor += 1 + window[slot] = add + slot += 1 + if slot == size { + slot = 0 + } + if hasher(add) != goal { + return false, -1 + } + for at, value := range verify { + if value != window[(cursor+1+int64(at))%limit] { + return false, -1 + } + } + return true, cursor - limit + 1 + } +} + +func Find(needle string) Seeker { + return makeSeeker([]byte(needle)) +} + +func Seedlings() map[uint64]int { + result := make(map[uint64]int) + for at, value := range seedlings { + result[value] = at + } + return result +} + +var seedlings = [256]uint64{ + 0x602b8129934c868b, 0x6ab8983d8c8ba01c, 0x54d04ec8dee35ff5, 0xfdc3567e6f901fd5, + 0x2562b4b5b0cc366c, 0xab60180bab46c43f, 0x5a54553f73bd3e1b, 0xaf194e0fe6c4bf87, + 0x25c98c0d6b63a370, 0x8c81fae8b37fff21, 0x53ead3efb8b5b25d, 0xcce5c646782cd9cf, + 0xfdf778b8ae365720, 0x4d977df169207cca, 0x156a5b75bc7798b5, 0x46c7f1335c2ed747, + 0x8310228569b88651, 0x35809eeb9ca50863, 0x441ee622f898ad0a, 0xc5bb8b2cf3932d6b, + 0xf7c606cabd736bc6, 0x28b4e86876eeedb9, 0xb49d9c3599dd647e, 0xda85b43fa53edcf2, + 0x392ee67addd0d02f, 0x73a31ef6d1b95fba, 0x5e169bbb3d28951b, 0x6969557639c6a9e8, + 0xb03c20f52a6f3fd4, 0x27ffff7cf607addd, 0x335c0951697ce069, 0xf40a8371a53c31dc, + 0x13a765069f736cc6, 0x942216377a8d67e3, 0xed0c5da61b167d3a, 0xaa9d5e228b5567a8, + 0x4cd448389d866d4f, 0x93d6a2e30f18e84f, 0xf8be8616379c8db1, 0x95c7c233cc922e36, + 0x8021cb619a850787, 0x7f6d5edea66f25a9, 0x0e6c9d1e6c9646f9, 0x02b9a1ab0b82bb32, + 0x8a4344782e76446f, 0xa93d1c7ff54f35cd, 0x303e4f8594da3e66, 0x8d034c3bcc43340e, + 0x70977337f51155a0, 0x750701470ef3de59, 0x3c57e01aeb3a9e59, 0xd583288188c6289a, + 0x656d0c50ea6bb54d, 0x76bbc1e73deb85ef, 0x40db7a12e5c065a3, 0xce585e8b46166d1d, + 0x284ee3e3fe8aa20b, 0x3f315e2464e9d196, 0x6f5b08ad33872bd9, 0x00b1405c606adb9e, + 0xbf20e1957769961a, 0x297ba8e2d1903af3, 0x6f903a275e60451e, 0x87a83971fb761024, + 0x9348cb5ff1383281, 0x2f203fec99682f8d, 0x1443e52adddcf08e, 0xef09ff3b737a55fc, + 0xe92cd8b27e851a79, 0x6e1e59a28c13f09a, 0x24a7c49a9bade515, 0xff3045ffc77d2b24, + 0xf16f51fa093ccdc6, 0xdf04734eec39671e, 0x76b6f9adb5c2d094, 0x3904a429219a48ef, + 0x9d0244a46fd87f84, 0x53177c2f2b3465d9, 0xf27b02832137d20b, 0x72a45d5c27ef2bde, + 0x8c8307bfc4117674, 0x69ca61ca73e8113d, 0x244eb5285055a241, 0xa57fa8ef3f85efc8, + 0x607d3dab80e04aa0, 0x7e47163b689e8c81, 0xcddc93876c73a1a8, 0x94d4635b1fcaa2e3, + 0xf1283c1bbc591b52, 0x54098cf2b11c8d68, 0x5b181f79ddc50186, 0x77c57d4e824a4636, + 0x39f888233f81fc4e, 0x5eb7eb313d175801, 0xad4b57e527f01949, 0xb91b4230e16f2edc, + 0x92a0676324bf9721, 0x384bf9513d1fb244, 0x40cf2ef187ef03cf, 0x6f12ddbbd8383773, + 0xa3110f4ead8a066a, 0x1b6ab1431567bb2d, 0x73a397be57c72c8a, 0x4c9c445d1db0c18c, + 0xc6cb2c15ed2b1faa, 0x83a988ce9c10d893, 0xaf4fe6805be23828, 0x776a74a4dec3cd7e, + 0xd23d9949d80c389b, 0xdbd6399f812a0c56, 0xc5bb1c90121ca7ce, 0x332cda9b9ff5afb9, + 0x5a6239acaafceabd, 0x0258f2053a726194, 0x142f72cfc81201cf, 0x85af1c1c2b3d0425, + 0x620568ce9f81d404, 0x28cc4d813b157eb7, 0x36637f0bc3f48ed8, 0x563c25e210789612, + 0x0ef4909420333fae, 0x8fd529de3bbc7a70, 0x8989297984fcc92d, 0x482f38b5985919bf, + 0x8b3604417bc20181, 0x8aa0bd889a5496cb, 0x38d69325a3b94aa1, 0x4b3d5b0c247c8316, + 0xc3bb81aa4a2b8f1e, 0x48baa02799ec924b, 0xd287c474ad6a3d0d, 0xf93312bf407b38a3, + 0x1a62a35cacbabca2, 0x266fdbb2033d743e, 0xac80c27e4a760ce1, 0x8995461d77a933c0, + 0x3ad00ba59d07b4f3, 0x969a9e95f9617ae3, 0x1079fe07d879543d, 0x31880d8a6420cb8c, + 0x84eba52cbad9d38a, 0x6477aa7aebf5d8b2, 0x31c5bcc065ae3124, 0x5e1b2121b423868e, + 0x3f208e19c74b0994, 0xc3ed021162e50000, 0x49a71a2f28d1732b, 0xcbe5d2df36846b2b, + 0xf3b59192f546f437, 0x0d826b0d72b2fd15, 0x6eeaa0c0a2ffb7f2, 0x6113e0030b7d5908, + 0x3659c2043e8aee58, 0xf1060b073baf9339, 0xc072daff6bc681f8, 0x884345ae6ef6c538, + 0x202184833fd0bbbf, 0xd266a3bb47fc22f1, 0x8914c38ffeaa392e, 0x73ce11a141170ea1, + 0xc0df84b348e03fbf, 0x6d745be300145ac8, 0x063b321ab6d0fad8, 0xf87bc2d24666b17e, + 0x320cd31b8df1ef33, 0xc1ec122e2fd5bb39, 0x74f5923d5d2b5eba, 0xe85798e5f5cf02c5, + 0x83360efbfe2ffae2, 0x79a0dba643e4b98a, 0x87512888e7e12293, 0xb1fd433d8a37043a, + 0xbad8d58a167d3ca5, 0x99855b2b011c29cc, 0x4fcfde91f1ef652a, 0xec462ce2c5b2a730, + 0xb2b03ac73d5de194, 0x9565e9662275f9aa, 0x1f5a09117923a94d, 0x201b3c78cc80bb5f, + 0x105da69edd31574f, 0x9444318eb5e5af8d, 0xb4bc0f4f23295ef7, 0x86988e0c3546863b, + 0x1827e710952a0df2, 0x84af4a83f577e63e, 0x477bee7db82f88d1, 0xc7808fa972ea660e, + 0x0904fe12acff3f63, 0xb5baf8db3fb767f1, 0xe675059b5b603db1, 0xcf34d51dbddd9733, + 0xf7dc0a20ebc5f184, 0xb49588039d34ee77, 0x3015fa3d8f3145f9, 0x26afeef62e3a9a29, + 0x174257d586d9a3f2, 0x9aa8876a6a5eafd5, 0xde5150df0c3eef53, 0x3ba44df0da99cb4a, + 0xcf8668774135287e, 0xe8abe6669fb8aa4d, 0xe8a72dcf45e862fa, 0xcf11e3d463f05295, + 0xdd8862d9877b910b, 0x1d4cc1684ee326fc, 0x98e7907b726a7b53, 0xad845110e13a3c48, + 0x37de32a13c2d959a, 0xecfbec7d1a2c4339, 0xb2334e53257814a9, 0xe287b65ac7e079d8, + 0x2f44ae0cecfd15ff, 0xf71e440c162d323f, 0x4ebc72f70a4438c4, 0x9972e1d375126584, + 0xdf43537388eecfb8, 0x93f1597d9f115c0b, 0x3e7c5ab2ce23d493, 0x01bcd6f5d1b559ae, + 0xae3a89d1c2ab3c97, 0x224c3ad9e70defa3, 0x8cc7be5774b6a234, 0x07c5a14d71baff0e, + 0xef36700d8e824543, 0x1a730b83ccac66bd, 0x2179a3d23e72bec5, 0xbdd5c3445c00db14, + 0xc9f22df506ca595d, 0x70c58e3b4b014d74, 0x938755c1c7e11634, 0x2ce1a74793a461bd, + 0xbf1f4a2f14f594ef, 0x95ec2bce4bf8481a, 0xe9d8809a5065a5b4, 0x6cec014177d56fbf, + 0xb68c5f723764db37, 0xa2f5f21314b3935f, 0x3d3289499254d863, 0x85a5b5667439987d, + 0x31710194f888f426, 0x61e713b19996c044, 0x922938c7acbf5a69, 0xb7d3bcf2f449d3b9, + 0x24401236b825b103, 0xb6067fe7627e6b5a, 0x74732e6e3fc80d22, 0xa172f36ec342041e, + 0xd1465ccc18cf6df3, 0xf4fb5eb5ccf459a2, 0x6b22cce719a2c185, 0xcf41f79318890218, + 0x1d65d8c00d13b16c, 0xf1d1fd3f8c5c3c81, 0xabf782ddcdc96476, 0x62aed2ded00413ca, +} diff --git a/trollhash/rrhash_test.go b/trollhash/rrhash_test.go new file mode 100644 index 00000000..bdce2cd8 --- /dev/null +++ b/trollhash/rrhash_test.go @@ -0,0 +1,60 @@ +package trollhash_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/trollhash" +) + +func TestUsingRollingHashWorks(t *testing.T) { + must, wont := hamlet.Specifications(t) + + must.Equal(12345, 12345^0) + must.Equal(9876543210, 9876543210^0) + + must.Equal(256, len(trollhash.Seedlings())) + + must.Equal(uint64(0x2f203fec99682f8d), trollhash.Hash([]byte("A"))) + must.Equal(uint64(0x1443e52adddcf08e), trollhash.Hash([]byte("B"))) + must.Equal(uint64(0xef09ff3b737a55fc), trollhash.Hash([]byte("C"))) + must.Equal(uint64(0x6d421a4e169d8ce7), trollhash.Hash([]byte("AB"))) + must.Equal(uint64(0x85192d4bc79632c7), trollhash.Hash([]byte("ABC"))) + + rolling := trollhash.Find("loha") + wont.Nil(rolling) + result := make([]int64, 0) + for _, step := range []byte("O aloha! Aloha, Holoham!") { + ok, at := rolling(step) + if ok { + result = append(result, at) + } + } + must.Equal([]int64{3, 10, 18}, result) + must.Equal(uint64(0xbe16aca9b15d96fa), trollhash.Hash([]byte("loha"))) + limit := 256 + for key := 0; key < 256; key++ { + result := make(map[uint64]bool) + flow := make([]byte, 0, limit) + for size := 0; size < limit; size++ { + flow = append(flow, byte(key)) + result[trollhash.Hash(flow)] = true + } + must.Equal(256, len(flow)) + must.True(63 < len(result)) + must.True(len(result) < 129) + } + limit = 10240 + uniques := make(map[uint64]bool) + flow := make([]byte, 0, limit) + source := rand.NewSource(time.Now().UnixNano()) + rnd := rand.New(source) + for count := 0; count < limit; count++ { + flow = append(flow, byte(rnd.Uint32())) + uniques[trollhash.Hash(flow)] = true + } + must.Equal(10240, len(flow)) + must.Equal(10240, len(uniques)) +} From 026e3d1e0aa53b1e78e2d1094647529e80882a59 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 31 Mar 2021 15:12:27 +0300 Subject: [PATCH 103/516] RCC-139: initial holotree integration (v9.9.1) - Github Actions upgrade to use Go 1.16 for rcc compilation --- .github/workflows/rcc.yaml | 7 ++++++- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index ac570531..d3f1778e 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -1,5 +1,7 @@ name: Rcc on: + workflow_dispatch: + # enables manual triggering push: branches: - master @@ -12,7 +14,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: '1.15.x' + go-version: '1.16.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' @@ -30,6 +32,9 @@ jobs: matrix: os: ['ubuntu'] steps: + - uses: actions/setup-go@v2 + with: + go-version: '1.16.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' diff --git a/common/version.go b/common/version.go index 443dd52e..2bd9ffa2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.0` + Version = `v9.9.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index ddd1e7ec..60e51c0d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.1 (date: 31.3.2021) + +- Github Actions upgrade to use Go 1.16 for rcc compilation + ## v9.9.0 (date: 31.3.2021) - added holotree as part of source code (but not as integrated part yet) From 96b2bc800a302d87877affad827b1a4b92c8b3c0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 1 Apr 2021 08:34:22 +0300 Subject: [PATCH 104/516] RCC-139: initial holotree integration (v9.9.2) - more holotree integration work to get it more experimentable --- cmd/{holotree.go => internalHolotree.go} | 48 ++++++++++++++---------- cmd/variables.go | 10 +++++ common/version.go | 2 +- docs/changelog.md | 4 ++ htfs/library.go | 4 ++ 5 files changed, 48 insertions(+), 20 deletions(-) rename cmd/{holotree.go => internalHolotree.go} (76%) diff --git a/cmd/holotree.go b/cmd/internalHolotree.go similarity index 76% rename from cmd/holotree.go rename to cmd/internalHolotree.go index 16875f39..b353d26d 100644 --- a/cmd/holotree.go +++ b/cmd/internalHolotree.go @@ -19,9 +19,11 @@ var ( holotreeBlueprint []byte holotreeProfiled string holotreeSpace string + holotreeForce bool + holotreeJson bool ) -var holotreeCmd = &cobra.Command{ +var internalHolotreeCmd = &cobra.Command{ Use: "holotree conda.yaml+", Aliases: []string{"htfs"}, Short: "Do holotree operations.", @@ -64,32 +66,32 @@ var holotreeCmd = &cobra.Command{ err = os.RemoveAll(tree.Stage()) pretty.Guard(err == nil, 3, "Failed to clean stage, reason %v.", err) + profiling := false + if holotreeProfiled != "" { + sink, err := os.Create(holotreeProfiled) + pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", holotreeProfiled, err) + defer sink.Close() + err = pprof.StartCPUProfile(sink) + pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) + profiling = true + } + common.Debug("Holotree stage is %q.", tree.Stage()) exists := tree.HasBlueprint(holotreeBlueprint) common.Debug("Has blueprint environment: %v", exists) - if !exists { + if holotreeForce || !exists { identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = ioutil.WriteFile(identityfile, holotreeBlueprint, 0o640) pretty.Guard(err == nil, 3, "Failed to save %q, reason %v.", identityfile, err) - label, err := conda.NewEnvironment(false, identityfile) + label, err := conda.NewEnvironment(holotreeForce, identityfile) pretty.Guard(err == nil, 3, "Failed to create environment, reason %v.", err) common.Debug("Label: %q", label) } anywork.Scale(17) - profiling := false - if holotreeProfiled != "" { - sink, err := os.Create(holotreeProfiled) - pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", holotreeProfiled, err) - defer sink.Close() - err = pprof.StartCPUProfile(sink) - pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) - profiling = true - } - - if !exists { + if holotreeForce || !exists { err := tree.Record(holotreeBlueprint) pretty.Guard(err == nil, 7, "Failed to record blueprint %q, reason: %v", string(holotreeBlueprint), err) } @@ -99,13 +101,21 @@ var holotreeCmd = &cobra.Command{ if profiling { pprof.StopCPUProfile() } - fmt.Fprintln(os.Stdout, path) + fmt.Fprintln(os.Stderr, path) + env := conda.EnvironmentExtensionFor(path) + if holotreeJson { + asJson(env) + } else { + asExportedText(env) + } }, } func init() { - internalCmd.AddCommand(holotreeCmd) - holotreeCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") - holotreeCmd.MarkFlagRequired("space") - holotreeCmd.Flags().StringVar(&holotreeProfiled, "profile", "", "Filename to save profiling information.") + internalCmd.AddCommand(internalHolotreeCmd) + internalHolotreeCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") + internalHolotreeCmd.MarkFlagRequired("space") + internalHolotreeCmd.Flags().StringVar(&holotreeProfiled, "profile", "", "Filename to save profiling information.") + internalHolotreeCmd.Flags().BoolVar(&holotreeForce, "force", false, "Force environment creation with refresh.") + internalHolotreeCmd.Flags().BoolVar(&holotreeJson, "json", false, "Show environment as JSON.") } diff --git a/cmd/variables.go b/cmd/variables.go index 5a700172..78ce6533 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -48,6 +48,16 @@ func asJson(items []string) error { return nil } +func asExportedText(items []string) { + prefix := "" + if !conda.IsWindows() { + prefix = "export " + } + for _, line := range items { + common.Stdout("%s%s\n", prefix, line) + } +} + func asText(items []string) { for _, line := range items { common.Stdout("%s\n", line) diff --git a/common/version.go b/common/version.go index 2bd9ffa2..73c4cf58 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.1` + Version = `v9.9.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 60e51c0d..a8e1834a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.2 (date: 1.4.2021) + +- more holotree integration work to get it more experimentable + ## v9.9.1 (date: 31.3.2021) - Github Actions upgrade to use Go 1.16 for rcc compilation diff --git a/htfs/library.go b/htfs/library.go index 8f9ecb84..b4a1823c 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -70,6 +70,7 @@ func (it *hololib) Stage() string { func (it *hololib) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() key := textual(sipit(blueprint), 0) + common.Timeline("holotree record start %s", key) fs, err := NewRoot(it.Stage()) if err != nil { return err @@ -85,6 +86,7 @@ func (it *hololib) Record(blueprint []byte) error { } score := &stats{} err = fs.Treetop(ScheduleLifters(it, score)) + defer common.Timeline("- new %d/%d", score.dirty, score.total) common.Debug("Holotree new workload: %d/%d\n", score.dirty, score.total) if err != nil { return err @@ -104,6 +106,7 @@ func (it *hololib) HasBlueprint(blueprint []byte) bool { func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() key := textual(sipit(blueprint), 0) + common.Timeline("holotree restore start %s", key) prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix @@ -126,6 +129,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { } score := &stats{} fs.AllDirs(RestoreDirectory(it, fs, score)) + defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) return where, nil } From 900bbe3512fb94aed53de6daec836148c93ad70f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 1 Apr 2021 12:06:11 +0300 Subject: [PATCH 105/516] RCC-139: initial holotree integration (v9.9.3) - added export/SET prefix to `rcc env variables` command - updated README.md with patterns to version numbered releases - known bug: holotree does not work correctly yet -- DO NOT USE --- README.md | 16 ++++++++++++++++ cmd/variables.go | 16 +++++----------- common/version.go | 2 +- docs/changelog.md | 6 ++++++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c3a941cc..3224b56d 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,22 @@ Upgrading: `brew upgrade rcc` *[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* +## Bleeding edge (or specific versions) + +If you want to live on the edge, and do not worry about little unstability, following table gives pattern for downloading specific versions of rcc. + +Or if you want stability and live on specific version of rcc, these patterns also apply. + +And you can find versions and details from [change log](/docs/changelog.md) file. + +| OS | Download URL (replace 0.0.0 with version you want to experiment with) | +| -------- | --------------------------------------------------------------------- | +| Windows | https://downloads.robocorp.com/rcc/releases/v0.0.0/windows64/rcc.exe | +| macOS | https://downloads.robocorp.com/rcc/releases/v0.0.0/macos64/rcc | +| Linux | https://downloads.robocorp.com/rcc/releases/v0.0.0/linux64/rcc | + +*[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* + ## Documentation Visit [https://robocorp.com/docs](https://robocorp.com/docs) to view the full documentation on the full Robocorp stack. diff --git a/cmd/variables.go b/cmd/variables.go index 78ce6533..f107c68e 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -49,18 +49,12 @@ func asJson(items []string) error { } func asExportedText(items []string) { - prefix := "" - if !conda.IsWindows() { - prefix = "export " + prefix := "export" + if conda.IsWindows() { + prefix = "SET" } for _, line := range items { - common.Stdout("%s%s\n", prefix, line) - } -} - -func asText(items []string) { - for _, line := range items { - common.Stdout("%s\n", line) + common.Stdout("%s %s\n", prefix, line) } } @@ -125,7 +119,7 @@ func exportEnvironment(condaYaml []string, packfile, taskName, environment, work return asJson(env) } - asText(env) + asExportedText(env) return nil } diff --git a/common/version.go b/common/version.go index 73c4cf58..11bd1711 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.2` + Version = `v9.9.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index a8e1834a..cdef99f4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.9.3 (date: 1.4.2021) + +- added export/SET prefix to `rcc env variables` command +- updated README.md with patterns to version numbered releases +- known bug: holotree does not work correctly yet -- DO NOT USE + ## v9.9.2 (date: 1.4.2021) - more holotree integration work to get it more experimentable From 4c822458a2b59aff985abd0a562760dc0585aac5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 6 Apr 2021 09:15:26 +0300 Subject: [PATCH 106/516] RCC-139: initial holotree integration (v9.9.4) - fix for holotree change detection when switching blueprints --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/functions.go | 20 ++++++++++++++++++-- htfs/library.go | 28 ++++++++++++++++++++-------- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/common/version.go b/common/version.go index 11bd1711..3701bd28 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.3` + Version = `v9.9.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index cdef99f4..a88e30ae 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.4 (date: 6.4.2021) + +- fix for holotree change detection when switching blueprints + ## v9.9.3 (date: 1.4.2021) - added export/SET prefix to `rcc env variables` command diff --git a/htfs/functions.go b/htfs/functions.go index d61ea5c5..24981cf0 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -13,6 +13,20 @@ import ( "github.com/robocorp/rcc/trollhash" ) +func DigestRecorder(target map[string]string) Treetop { + var tool Treetop + tool = func(path string, it *Dir) error { + for name, subdir := range it.Dirs { + tool(filepath.Join(path, name), subdir) + } + for name, file := range it.Files { + target[filepath.Join(path, name)] = file.Digest + } + return nil + } + return tool +} + func Locator(seek string) Filetask { return func(fullpath string, details *File) anywork.Work { return func() { @@ -158,7 +172,7 @@ func RemoveDirectory(dirname string) anywork.Work { } } -func RestoreDirectory(library Library, fs *Root, stats *stats) Dirtask { +func RestoreDirectory(library Library, fs *Root, current map[string]string, stats *stats) Dirtask { return func(path string, it *Dir) anywork.Work { return func() { source, err := os.Open(path) @@ -196,7 +210,9 @@ func RestoreDirectory(library Library, fs *Root, stats *stats) Dirtask { //fmt.Println("Extra file, remove:", directpath) continue } - ok = found.Match(info) + shadow, ok := current[directpath] + golden := !ok || found.Digest == shadow + ok = golden && found.Match(info) stats.Dirty(!ok) if !ok { directory := library.Location(found.Digest) diff --git a/htfs/library.go b/htfs/library.go index b4a1823c..109e80c0 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -79,7 +79,9 @@ func (it *hololib) Record(blueprint []byte) error { if err != nil { return err } + common.Timeline("holotree (re)locator start") fs.AllFiles(Locator(it.Identity())) + common.Timeline("holotree (re)locator done") err = fs.SaveAs(filepath.Join(it.basedir, "hololib", "catalog", key)) if err != nil { return err @@ -88,10 +90,7 @@ func (it *hololib) Record(blueprint []byte) error { err = fs.Treetop(ScheduleLifters(it, score)) defer common.Timeline("- new %d/%d", score.dirty, score.total) common.Debug("Holotree new workload: %d/%d\n", score.dirty, score.total) - if err != nil { - return err - } - return os.RemoveAll(it.Stage()) + return err } func (it *hololib) CatalogPath(key string) string { @@ -110,6 +109,16 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix + metafile := filepath.Join(it.basedir, "holotree", fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(it.basedir, "holotree", name) + currentstate := make(map[string]string) + shadow, err := NewRoot(targetdir) + if err == nil { + err = shadow.LoadFrom(metafile) + } + if err == nil { + shadow.Treetop(DigestRecorder(currentstate)) + } fs, err := NewRoot(it.Stage()) if err != nil { return "", err @@ -118,8 +127,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { if err != nil { return "", err } - where := filepath.Join(it.basedir, "holotree", name) - err = fs.Relocate(where) + err = fs.Relocate(targetdir) if err != nil { return "", err } @@ -128,10 +136,14 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { return "", err } score := &stats{} - fs.AllDirs(RestoreDirectory(it, fs, score)) + fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) - return where, nil + err = fs.SaveAs(metafile) + if err != nil { + return "", err + } + return targetdir, nil } func sipit(key []byte) uint64 { From ac9736a85b1c7344ec28ad9d36be0f8f05f978e1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 6 Apr 2021 09:36:25 +0300 Subject: [PATCH 107/516] UPGRADE: newer micromamba upgrade (v9.9.5) --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 4 ++++ 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 3701bd28..f3a6e7b5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.4` + Version = `v9.9.5` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 2482304e..ddfe1b0e 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.8.2/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.9.2/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 60b8f22d..4d33cf85 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.8.2/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.9.2/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 2ec10aee..694777e4 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.8.2/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.9.2/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 3ed34405..403c9bec 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -237,7 +237,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 8002 + goodEnough := version >= 9002 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) return goodEnough } diff --git a/conda/workflows.go b/conda/workflows.go index 968b9075..1c61f844 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -191,7 +191,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "fail", "--retry-with-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) + mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index a88e30ae..34f75fad 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.5 (date: 30.3.2021) + +- micromamba upgrade to version 0.9.2 + ## v9.9.4 (date: 6.4.2021) - fix for holotree change detection when switching blueprints From 9553cf5b22a74d2a452f45d15582c70214c7d56e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 8 Apr 2021 11:16:00 +0300 Subject: [PATCH 108/516] RCC-139: initial holotree integration (v9.9.6) - holotree note: in this series 9, holotree will remain experimental and will not be used for production yet - added separate `holotree` subtree in command structure (it is not internal anymore, but still hidden) - partial implementations of holotree variables and bootstrap commands - settings.yaml version 2021.04 update: now there is separate section for templates - profiling option `--pprof` is now global level option - improved error message when rcc is not configured yet --- assets/settings.yaml | 7 ++- cloud/client.go | 56 ++++++++++++++++++ cmd/assistantList.go | 2 +- cmd/assistantRun.go | 2 +- cmd/cloudNew.go | 2 +- cmd/credentials.go | 2 +- cmd/download.go | 2 +- cmd/holotree.go | 17 ++++++ cmd/holotreeBootstrap.go | 53 +++++++++++++++++ cmd/holotreeVariables.go | 74 +++++++++++++++++++++++ cmd/internalHolotree.go | 121 -------------------------------------- cmd/internale2ee.go | 4 +- cmd/pull.go | 2 +- cmd/push.go | 2 +- cmd/rcc/main.go | 3 +- cmd/root.go | 27 ++++++++- cmd/upload.go | 2 +- cmd/userinfo.go | 2 +- cmd/workspace.go | 2 +- common/timeline.go | 5 ++ common/variables.go | 6 +- common/version.go | 2 +- conda/cleanup.go | 4 +- conda/download.go | 58 ------------------ conda/installing.go | 2 +- conda/robocorp.go | 6 +- docs/changelog.md | 14 ++++- fail/handling.go | 32 ++++++++++ htfs/commands.go | 45 ++++++++++++++ operations/authorize.go | 2 +- operations/credentials.go | 8 ++- settings/data.go | 13 ++-- settings/settings.go | 6 ++ xviper/wrapper.go | 4 ++ 34 files changed, 376 insertions(+), 213 deletions(-) create mode 100644 cmd/holotree.go create mode 100644 cmd/holotreeBootstrap.go create mode 100644 cmd/holotreeVariables.go delete mode 100644 cmd/internalHolotree.go delete mode 100644 conda/download.go create mode 100644 fail/handling.go create mode 100644 htfs/commands.go diff --git a/assets/settings.yaml b/assets/settings.yaml index ab286f57..c6cb1138 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -28,6 +28,11 @@ branding: logo: https://downloads.robocorp.com/company/press-kit/logos/robocorp-logo-black.svg theme-color: FF0000 +templates: + standard: Standard Robot Framework template + extended: Extended Robot Framework template + python: Basic Python template + meta: source: builtin - version: 2021.03 + version: 2021.04 diff --git a/cloud/client.go b/cloud/client.go index 76130687..af1f0e1e 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -1,13 +1,17 @@ package cloud import ( + "crypto/sha256" "fmt" "io" "io/ioutil" "net/http" + "os" + "path/filepath" "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -141,3 +145,55 @@ func (it *internalClient) Put(request *Request) *Response { func (it *internalClient) Delete(request *Request) *Response { return it.does("DELETE", request) } + +func Download(url, filename string) error { + common.Timeline("start %s download", filename) + defer common.Timeline("done %s download", filename) + + if pathlib.Exists(filename) { + err := os.Remove(filename) + if err != nil { + return err + } + } + + client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + request.Header.Add("Accept", "application/octet-stream") + response, err := client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return fmt.Errorf("Downloading %q failed, reason: %q!", url, response.Status) + } + + pathlib.EnsureDirectory(filepath.Dir(filename)) + out, err := os.Create(filename) + if err != nil { + return err + } + defer out.Close() + + digest := sha256.New() + many := io.MultiWriter(out, digest) + + common.Debug("Downloading %s <%s> -> %s", url, response.Status, filename) + + _, err = io.Copy(many, response.Body) + if err != nil { + return err + } + + err = out.Sync() + if err != nil { + return err + } + + return common.Debug("%q SHA256 sum: %02x", filename, digest.Sum(nil)) +} diff --git a/cmd/assistantList.go b/cmd/assistantList.go index 252d8e3e..c37b34cd 100644 --- a/cmd/assistantList.go +++ b/cmd/assistantList.go @@ -22,7 +22,7 @@ var assistantListCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 57a31f33..1f39b2c6 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -37,7 +37,7 @@ var assistantRunCmd = &cobra.Command{ defer xviper.RunMinutes().Done() account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index 52826e48..6adbb5f8 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -19,7 +19,7 @@ var newCloudCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/credentials.go b/cmd/credentials.go index 50a29eb1..66f1d05a 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -68,7 +68,7 @@ var credentialsCmd = &cobra.Command{ func localDelete(accountName string) { account := operations.AccountByName(accountName) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", accountName) + pretty.Exit(1, "Could not find account by name: %q", accountName) } err := account.Delete() if err != nil { diff --git a/cmd/download.go b/cmd/download.go index aebbfe5d..a2199c68 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -19,7 +19,7 @@ var downloadCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/holotree.go b/cmd/holotree.go new file mode 100644 index 00000000..2161c60b --- /dev/null +++ b/cmd/holotree.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var holotreeCmd = &cobra.Command{ + Use: "holotree", + Aliases: []string{"ht"}, + Short: "Group of holotree commands.", + Long: "Group of holotree commands.", + Hidden: true, +} + +func init() { + rootCmd.AddCommand(holotreeCmd) +} diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go new file mode 100644 index 00000000..cfeb64c7 --- /dev/null +++ b/cmd/holotreeBootstrap.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" + "github.com/spf13/cobra" +) + +var holotreeBootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Aliases: []string{"boot"}, + Short: "Bootstrap holotree from set of templates.", + Long: "Bootstrap holotree from set of templates.", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree bootstrap lasted").Report() + } + + ok := conda.MustMicromamba() + pretty.Guard(ok, 1, "Could not get micromamba installed.") + + robots := make([]string, 0, 20) + for key, _ := range settings.Global.Templates() { + zipname := fmt.Sprintf("%s.zip", key) + filename := filepath.Join(common.TemplateLocation(), zipname) + robots = append(robots, filename) + url := fmt.Sprintf("templates/%s", zipname) + err := cloud.Download(settings.Global.DownloadsLink(url), filename) + pretty.Guard(err == nil, 2, "Could not download %q, reason: %w", url, err) + } + + for at, robot := range robots { + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) + defer os.RemoveAll(workarea) + common.Debug("Using temporary workarea: %v", workarea) + err := operations.Unzip(workarea, robot, false, true) + pretty.Guard(err == nil, 2, "Could not unzip %q, reason: %w", robot, err) + } + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeBootstrapCmd) +} diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go new file mode 100644 index 00000000..5256964c --- /dev/null +++ b/cmd/holotreeVariables.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + holotreeBlueprint []byte + holotreeSpace string + holotreeForce bool + holotreeJson bool +) + +var holotreeVariablesCmd = &cobra.Command{ + Use: "variables conda.yaml+", + Aliases: []string{"vars"}, + Short: "Do holotree operations.", + Long: "Do holotree operations.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree command lasted").Report() + } + + var left, right *conda.Environment + var err error + + for _, filename := range args { + left = right + right, err = conda.ReadCondaYaml(filename) + pretty.Guard(err == nil, 1, "Failure: %v", err) + if left == nil { + continue + } + right, err = left.Merge(right) + pretty.Guard(err == nil, 1, "Failure: %v", err) + } + pretty.Guard(right != nil, 1, "Missing environment specification(s).") + content, err := right.AsYaml() + pretty.Guard(err == nil, 1, "YAML error: %v", err) + holotreeBlueprint = []byte(content) + + ok := conda.MustMicromamba() + pretty.Guard(ok, 1, "Could not get micromamba installed.") + + anywork.Scale(25) + + tree, err := htfs.RecordEnvironment(holotreeBlueprint, holotreeForce) + pretty.Guard(err == nil, 2, "%w", err) + + path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(holotreeSpace)) + pretty.Guard(err == nil, 10, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + + env := conda.EnvironmentExtensionFor(path) + if holotreeJson { + asJson(env) + } else { + asExportedText(env) + } + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeVariablesCmd) + holotreeVariablesCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") + holotreeVariablesCmd.MarkFlagRequired("space") + holotreeVariablesCmd.Flags().BoolVar(&holotreeForce, "force", false, "Force environment creation with refresh.") + holotreeVariablesCmd.Flags().BoolVar(&holotreeJson, "json", false, "Show environment as JSON.") +} diff --git a/cmd/internalHolotree.go b/cmd/internalHolotree.go deleted file mode 100644 index b353d26d..00000000 --- a/cmd/internalHolotree.go +++ /dev/null @@ -1,121 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime/pprof" - - "github.com/robocorp/rcc/anywork" - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/htfs" - "github.com/robocorp/rcc/pretty" - "github.com/spf13/cobra" -) - -var ( - holotreeBlueprint []byte - holotreeProfiled string - holotreeSpace string - holotreeForce bool - holotreeJson bool -) - -var internalHolotreeCmd = &cobra.Command{ - Use: "holotree conda.yaml+", - Aliases: []string{"htfs"}, - Short: "Do holotree operations.", - Long: "Do holotree operations.", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Holotree command lasted").Report() - } - - var left, right *conda.Environment - var err error - - for _, filename := range args { - left = right - right, err = conda.ReadCondaYaml(filename) - pretty.Guard(err == nil, 1, "Failure: %v", err) - if left == nil { - continue - } - right, err = left.Merge(right) - pretty.Guard(err == nil, 1, "Failure: %v", err) - } - pretty.Guard(right != nil, 1, "Missing environment specification(s).") - content, err := right.AsYaml() - pretty.Guard(err == nil, 1, "YAML error: %v", err) - holotreeBlueprint = []byte(content) - - ok := conda.MustMicromamba() - pretty.Guard(ok, 1, "Could not get micromamba installed.") - - tree, err := htfs.New(common.RobocorpHome()) - pretty.Guard(err == nil, 2, "Failed to create holotree location, reason %v.", err) - - // following must be setup here - common.StageFolder = tree.Stage() - common.Stageonly = true - common.Liveonly = true - - err = os.RemoveAll(tree.Stage()) - pretty.Guard(err == nil, 3, "Failed to clean stage, reason %v.", err) - - profiling := false - if holotreeProfiled != "" { - sink, err := os.Create(holotreeProfiled) - pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", holotreeProfiled, err) - defer sink.Close() - err = pprof.StartCPUProfile(sink) - pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) - profiling = true - } - - common.Debug("Holotree stage is %q.", tree.Stage()) - exists := tree.HasBlueprint(holotreeBlueprint) - common.Debug("Has blueprint environment: %v", exists) - - if holotreeForce || !exists { - identityfile := filepath.Join(tree.Stage(), "identity.yaml") - err = ioutil.WriteFile(identityfile, holotreeBlueprint, 0o640) - pretty.Guard(err == nil, 3, "Failed to save %q, reason %v.", identityfile, err) - label, err := conda.NewEnvironment(holotreeForce, identityfile) - pretty.Guard(err == nil, 3, "Failed to create environment, reason %v.", err) - common.Debug("Label: %q", label) - } - - anywork.Scale(17) - - if holotreeForce || !exists { - err := tree.Record(holotreeBlueprint) - pretty.Guard(err == nil, 7, "Failed to record blueprint %q, reason: %v", string(holotreeBlueprint), err) - } - - path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(holotreeSpace)) - pretty.Guard(err == nil, 10, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) - if profiling { - pprof.StopCPUProfile() - } - fmt.Fprintln(os.Stderr, path) - env := conda.EnvironmentExtensionFor(path) - if holotreeJson { - asJson(env) - } else { - asExportedText(env) - } - }, -} - -func init() { - internalCmd.AddCommand(internalHolotreeCmd) - internalHolotreeCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") - internalHolotreeCmd.MarkFlagRequired("space") - internalHolotreeCmd.Flags().StringVar(&holotreeProfiled, "profile", "", "Filename to save profiling information.") - internalHolotreeCmd.Flags().BoolVar(&holotreeForce, "force", false, "Force environment creation with refresh.") - internalHolotreeCmd.Flags().BoolVar(&holotreeJson, "json", false, "Show environment as JSON.") -} diff --git a/cmd/internale2ee.go b/cmd/internale2ee.go index d0ca2c2c..6d4226c7 100644 --- a/cmd/internale2ee.go +++ b/cmd/internale2ee.go @@ -35,7 +35,7 @@ var e2eeCmd = &cobra.Command{ func version1encryption(args []string) { account := operations.AccountByName(AccountName()) - pretty.Guard(account != nil, 1, "Could not find account by name: %v", AccountName()) + pretty.Guard(account != nil, 1, "Could not find account by name: %q", AccountName()) client, err := cloud.NewClient(account.Endpoint) pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) @@ -58,7 +58,7 @@ func version1encryption(args []string) { func version2encryption(args []string) { account := operations.AccountByName(AccountName()) - pretty.Guard(account != nil, 1, "Could not find account by name: %v", AccountName()) + pretty.Guard(account != nil, 1, "Could not find account by name: %q", AccountName()) client, err := cloud.NewClient(account.Endpoint) pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) diff --git a/cmd/pull.go b/cmd/pull.go index c9ee4ff3..78fafd36 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -24,7 +24,7 @@ var pullCmd = &cobra.Command{ account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) diff --git a/cmd/push.go b/cmd/push.go index 27ddadf6..6364f7e6 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -23,7 +23,7 @@ var pushCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 6db29086..92659559 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -62,11 +62,12 @@ func markTempForRecycling() { func main() { common.Timeline("Start.") + defer ExitProtection() + defer common.EndOfTimeline() go startTempRecycling() defer markTempForRecycling() defer os.Stderr.Sync() defer os.Stdout.Sync() - defer ExitProtection() cmd.Execute() } diff --git a/cmd/root.go b/cmd/root.go index eb0a52e7..2b1598a4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime/pprof" "strings" "github.com/robocorp/rcc/common" @@ -15,6 +16,11 @@ import ( "github.com/spf13/cobra" ) +var ( + profilefile string + profiling *os.File +) + func toplevelCommands(parent *cobra.Command) { common.Log("\nToplevel commands") for _, child := range parent.Commands() { @@ -68,14 +74,22 @@ func Origin() string { } func Execute() { - if err := rootCmd.Execute(); err != nil { - pretty.Exit(1, "Error: [rcc %v] %v", common.Version, err) - } + defer func() { + if profiling != nil { + pprof.StopCPUProfile() + profiling.Sync() + profiling.Close() + } + }() + + err := rootCmd.Execute() + pretty.Guard(err == nil, 1, "Error: [rcc %v] %v", common.Version, err) } func init() { cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&profilefile, "pprof", "", "Filename to save profiling information.") rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP/rcc.yaml)") @@ -92,6 +106,13 @@ func init() { } func initConfig() { + if profilefile != "" { + sink, err := os.Create(profilefile) + pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", profilefile, err) + err = pprof.StartCPUProfile(sink) + pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) + profiling = sink + } if cfgFile != "" { xviper.SetConfigFile(cfgFile) } else { diff --git a/cmd/upload.go b/cmd/upload.go index f64b5e5f..23609490 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -19,7 +19,7 @@ var uploadCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/userinfo.go b/cmd/userinfo.go index 5735a8ec..37238aee 100644 --- a/cmd/userinfo.go +++ b/cmd/userinfo.go @@ -22,7 +22,7 @@ var userinfoCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Error: Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Error: Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/cmd/workspace.go b/cmd/workspace.go index 6d73ab92..8e2499ce 100644 --- a/cmd/workspace.go +++ b/cmd/workspace.go @@ -21,7 +21,7 @@ var workspaceCmd = &cobra.Command{ } account := operations.AccountByName(AccountName()) if account == nil { - pretty.Exit(1, "Could not find account by name: %v", AccountName()) + pretty.Exit(1, "Could not find account by name: %q", AccountName()) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/common/timeline.go b/common/timeline.go index cd8767bc..810e0d55 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -45,7 +45,12 @@ func init() { go timeliner(pipe, done) } +func IgnoreAllPanics() { + recover() +} + func Timeline(form string, details ...interface{}) { + defer IgnoreAllPanics() pipe <- fmt.Sprintf(form, details...) } diff --git a/common/variables.go b/common/variables.go index d64d027a..cc4ccc7c 100644 --- a/common/variables.go +++ b/common/variables.go @@ -47,6 +47,10 @@ func ensureDirectory(name string) string { return name } +func TemplateLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "templates")) +} + func BinLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "bin")) } @@ -55,7 +59,7 @@ func LiveLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "live")) } -func TemplateLocation() string { +func BaseLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "base")) } diff --git a/common/version.go b/common/version.go index f3a6e7b5..5f853b47 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.5` + Version = `v9.9.6` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 0c0cc527..a00a0039 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -64,7 +64,7 @@ func spotlessCleanup(dryrun bool) error { } if dryrun { common.Log("Would be removing:") - common.Log("- %v", common.TemplateLocation()) + common.Log("- %v", common.BaseLocation()) common.Log("- %v", common.LiveLocation()) common.Log("- %v", common.PipCache()) common.Log("- %v", MambaPackages()) @@ -72,7 +72,7 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", RobocorpTempRoot()) return nil } - safeRemove("cache", common.TemplateLocation()) + safeRemove("cache", common.BaseLocation()) safeRemove("cache", common.LiveLocation()) safeRemove("cache", common.PipCache()) safeRemove("cache", MambaPackages()) diff --git a/conda/download.go b/conda/download.go deleted file mode 100644 index fef4a51f..00000000 --- a/conda/download.go +++ /dev/null @@ -1,58 +0,0 @@ -package conda - -import ( - "crypto/sha256" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/settings" -) - -func DownloadMicromamba() error { - common.Timeline("downloading micromamba") - url := MicromambaLink() - filename := BinMicromamba() - client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - request.Header.Add("Accept", "application/octet-stream") - response, err := client.Do(request) - if err != nil { - return err - } - defer response.Body.Close() - - if response.StatusCode < 200 || response.StatusCode >= 300 { - return fmt.Errorf("Downloading %q failed, reason: %q!", url, response.Status) - } - - if pathlib.Exists(BinMicromamba()) { - os.Remove(BinMicromamba()) - } - - pathlib.EnsureDirectory(filepath.Dir(BinMicromamba())) - out, err := os.Create(filename) - if err != nil { - return err - } - defer out.Close() - - digest := sha256.New() - many := io.MultiWriter(out, digest) - - common.Debug("Downloading %s <%s> -> %s", url, response.Status, filename) - - _, err = io.Copy(many, response.Body) - if err != nil { - return err - } - - return common.Debug("SHA256 sum: %02x", digest.Sum(nil)) -} diff --git a/conda/installing.go b/conda/installing.go index 4d2dfa9f..c7d632a3 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -21,7 +21,7 @@ func DoDownload(delay time.Duration) bool { time.Sleep(delay) - err := DownloadMicromamba() + err := cloud.Download(MicromambaLink(), BinMicromamba()) if err != nil { common.Fatal("Download", err) os.Remove(BinMicromamba()) diff --git a/conda/robocorp.go b/conda/robocorp.go index 403c9bec..6de58254 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -278,7 +278,7 @@ func LocalChannel() (string, bool) { } func TemplateFrom(hash string) string { - return filepath.Join(common.TemplateLocation(), hash) + return filepath.Join(common.BaseLocation(), hash) } func LiveFrom(hash string) string { @@ -289,7 +289,7 @@ func LiveFrom(hash string) string { } func TemplateList() []string { - return dirnamesFrom(common.TemplateLocation()) + return dirnamesFrom(common.BaseLocation()) } func LiveList() []string { @@ -297,7 +297,7 @@ func LiveList() []string { } func OrphanList() []string { - result := orphansFrom(common.TemplateLocation()) + result := orphansFrom(common.BaseLocation()) result = append(result, orphansFrom(common.LiveLocation())...) return result } diff --git a/docs/changelog.md b/docs/changelog.md index 34f75fad..94d534e5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,18 @@ # rcc change log -## v9.9.5 (date: 30.3.2021) +## v9.9.6 (date: 8.4.2021) + +- holotree note: in this series 9, holotree will remain experimental and + will not be used for production yet +- added separate `holotree` subtree in command structure (it is not internal + anymore, but still hidden) +- partial implementations of holotree variables and bootstrap commands +- settings.yaml version 2021.04 update: now there is separate section + for templates +- profiling option `--pprof` is now global level option +- improved error message when rcc is not configured yet + +## v9.9.5 (date: 6.4.2021) - micromamba upgrade to version 0.9.2 diff --git a/fail/handling.go b/fail/handling.go new file mode 100644 index 00000000..09a460e2 --- /dev/null +++ b/fail/handling.go @@ -0,0 +1,32 @@ +package fail + +import "fmt" + +func Around(err *error) { + original := recover() + if original == nil { + return + } + + catch, ok := original.(delimited) + if !ok { + panic(original) + } + + *err = catch() +} + +func On(condition bool, form string, details ...interface{}) { + if condition { + panic(failure(form, details...)) + } +} + +func failure(form string, details ...interface{}) delimited { + err := fmt.Errorf(form, details...) + return func() error { + return err + } +} + +type delimited func() error diff --git a/htfs/commands.go b/htfs/commands.go new file mode 100644 index 00000000..f8815081 --- /dev/null +++ b/htfs/commands.go @@ -0,0 +1,45 @@ +package htfs + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/fail" +) + +func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { + defer fail.Around(&err) + + tree, err := New(common.RobocorpHome()) + fail.On(err != nil, "Failed to create holotree location, reason %w.", err) + + // following must be setup here + common.StageFolder = tree.Stage() + common.Stageonly = true + common.Liveonly = true + + err = os.RemoveAll(tree.Stage()) + fail.On(err != nil, "Failed to clean stage, reason %w.", err) + + common.Debug("Holotree stage is %q.", tree.Stage()) + exists := tree.HasBlueprint(blueprint) + common.Debug("Has blueprint environment: %v", exists) + + if force || !exists { + identityfile := filepath.Join(tree.Stage(), "identity.yaml") + err = ioutil.WriteFile(identityfile, blueprint, 0o640) + fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) + label, err := conda.NewEnvironment(force, identityfile) + fail.On(err != nil, "Failed to create environment, reason %w.", err) + common.Debug("Label: %q", label) + } + + if force || !exists { + err := tree.Record(blueprint) + fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) + } + return tree, nil +} diff --git a/operations/authorize.go b/operations/authorize.go index d84acd65..df164406 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -174,7 +174,7 @@ func HmacSignature(claims *Claims, secret, nonce, bodyHash string) string { func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { account := AccountByName(accountName) if account == nil { - return nil, fmt.Errorf("Could not find account by name: %s", accountName) + return nil, fmt.Errorf("Could not find account by name: %q", accountName) } client, err := cloud.NewClient(account.Endpoint) if err != nil { diff --git a/operations/credentials.go b/operations/credentials.go index 0a4f8f29..17afc3de 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -84,6 +85,10 @@ func VerifyAccounts(force bool) { } } +func (it *account) IsValid() bool { + return len(it.Endpoint) > 0 && len(it.Account) > 0 +} + func (it *account) CacheKey() string { return fmt.Sprintf("%s.%s", it.Identifier, it.Secret[:6]) } @@ -237,11 +242,12 @@ func AccountByName(label string) *account { if dynamic != nil { return createEphemeralAccount(dynamic) } + pretty.Guard(xviper.IsAvailable(), 1, "This rcc is not configured yet. Please, fix that first.") if len(label) == 0 { label = DefaultAccountName() } found := loadAccount(label) - if found.Account == label { + if found.Account == label && found.IsValid() { return found } return nil diff --git a/settings/data.go b/settings/data.go index 892cc22d..a29e9939 100644 --- a/settings/data.go +++ b/settings/data.go @@ -17,12 +17,13 @@ const ( type StringMap map[string]string type Settings struct { - Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` - Branding StringMap `yaml:"branding" json:"branding"` - Certificates *Certificates `yaml:"certificates" json:"certificates"` - Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` - Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` - Meta *Meta `yaml:"meta" json:"meta"` + Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` + Branding StringMap `yaml:"branding" json:"branding"` + Certificates *Certificates `yaml:"certificates" json:"certificates"` + Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` + Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` + Templates map[string]string `yaml:"templates" json:"templates"` + Meta *Meta `yaml:"meta" json:"meta"` } func FromBytes(raw []byte) (*Settings, error) { diff --git a/settings/settings.go b/settings/settings.go index 5e6ea250..82a53872 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -160,6 +160,12 @@ func (it gateway) Hostnames() []string { return config.Hostnames() } +func (it gateway) Templates() map[string]string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Templates +} + func (it gateway) ConfiguredHttpTransport() *http.Transport { return httpTransport } diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 1ef8facf..93aa3b19 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -110,6 +110,10 @@ func runner(todo <-chan command) { } } +func IsAvailable() bool { + return len(AllKeys()) > 0 +} + func SetConfigFile(in string) { pipeline <- func(core *config) { core.Reset(in) From 5ef63bb619ce04a42fa5c979d8ada365e54c89de Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 8 Apr 2021 13:08:44 +0300 Subject: [PATCH 109/516] RCC-139: initial holotree integration (v9.9.7) - now `rcc holotree bootstrap` can only download templates with `--quick` flag, or otherwise also prepare environment based on that template --- cmd/holotreeBootstrap.go | 38 ++++++++++++++++++++++++++++++++------ common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index cfeb64c7..6f11086a 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -8,12 +8,39 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) +var ( + holotreeQuick bool +) + +func updateEnvironments(robots []string) { + for at, robotling := range robots { + workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) + defer os.RemoveAll(workarea) + common.Debug("Using temporary workarea: %v", workarea) + err := operations.Unzip(workarea, robotling, false, true) + pretty.Guard(err == nil, 2, "Could not unzip %q, reason: %w", robotling, err) + targetRobot := robot.DetectConfigurationName(workarea) + config, err := robot.LoadRobotYaml(targetRobot, false) + pretty.Guard(err == nil, 2, "Could not load robot config %q, reason: %w", targetRobot, err) + condafile := config.CondaConfigFile() + right, err := conda.ReadCondaYaml(condafile) + pretty.Guard(err == nil, 2, "Could not load environmet config %q, reason: %w", condafile, err) + content, err := right.AsYaml() + pretty.Guard(err == nil, 2, "YAML error: %v", err) + holotreeBlueprint := []byte(content) + _, err = htfs.RecordEnvironment(holotreeBlueprint, false) + pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) + } +} + var holotreeBootstrapCmd = &cobra.Command{ Use: "bootstrap", Aliases: []string{"boot"}, @@ -38,16 +65,15 @@ var holotreeBootstrapCmd = &cobra.Command{ pretty.Guard(err == nil, 2, "Could not download %q, reason: %w", url, err) } - for at, robot := range robots { - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) - defer os.RemoveAll(workarea) - common.Debug("Using temporary workarea: %v", workarea) - err := operations.Unzip(workarea, robot, false, true) - pretty.Guard(err == nil, 2, "Could not unzip %q, reason: %w", robot, err) + if !holotreeQuick { + updateEnvironments(robots) } + + pretty.Ok() }, } func init() { holotreeCmd.AddCommand(holotreeBootstrapCmd) + holotreeBootstrapCmd.Flags().BoolVar(&holotreeQuick, "quick", false, "Do not create environments, just download templates.") } diff --git a/common/version.go b/common/version.go index 5f853b47..cbc4616c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.6` + Version = `v9.9.7` ) diff --git a/docs/changelog.md b/docs/changelog.md index 94d534e5..f1ba8efd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.7 (date: 8.4.2021) + +- now `rcc holotree bootstrap` can only download templates with `--quick` + flag, or otherwise also prepare environment based on that template + ## v9.9.6 (date: 8.4.2021) - holotree note: in this series 9, holotree will remain experimental and From 4af8a8d347ecbe2f5e3530e7a39934c56eea61fb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 9 Apr 2021 07:26:18 +0300 Subject: [PATCH 110/516] RCC-139: initial holotree integration (v9.9.8) - skip environment bootstrap when there is no conda.yaml used - added index.py utility tool for generating index.html for S3 --- .github/workflows/rcc.yaml | 4 +- Rakefile | 10 +++- cmd/holotreeBootstrap.go | 10 ++-- common/version.go | 2 +- docs/changelog.md | 7 ++- docs/index.py | 116 +++++++++++++++++++++++++++++++++++++ htfs/commands.go | 12 ++++ 7 files changed, 150 insertions(+), 11 deletions(-) create mode 100755 docs/index.py diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index d3f1778e..f9e23653 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -22,7 +22,7 @@ jobs: - name: What run: rake what - name: Building - run: rake build + run: rake clean build robot: name: Robot @@ -47,7 +47,7 @@ jobs: - name: What run: rake what - name: Testing - run: rake robot + run: rake clean robot - uses: actions/upload-artifact@v1 if: success() || failure() with: diff --git a/Rakefile b/Rakefile index 29a34715..57c5aee0 100644 --- a/Rakefile +++ b/Rakefile @@ -33,6 +33,10 @@ task :assets do sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md" end +task :clean do + sh 'rm -rf build/' +end + task :support do sh 'mkdir -p tmp build/linux64 build/macos64 build/windows64' end @@ -81,7 +85,7 @@ task :robot => :local do end desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do +task :build => [:tooling, :version_txt, :index_html, :linux64, :macos64, :windows64] do sh 'ls -l $(find build -type f)' end @@ -89,6 +93,10 @@ def version `sed -n -e '/Version/{s/^.*\`v//;s/\`$//p}' common/version.go`.strip end +task :index_html => :support do + sh 'docs/index.py > build/index.html' +end + task :version_txt => :support do File.write('build/version.txt', "v#{version}") end diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 6f11086a..17467ed5 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -30,13 +30,11 @@ func updateEnvironments(robots []string) { targetRobot := robot.DetectConfigurationName(workarea) config, err := robot.LoadRobotYaml(targetRobot, false) pretty.Guard(err == nil, 2, "Could not load robot config %q, reason: %w", targetRobot, err) + if !config.UsesConda() { + continue + } condafile := config.CondaConfigFile() - right, err := conda.ReadCondaYaml(condafile) - pretty.Guard(err == nil, 2, "Could not load environmet config %q, reason: %w", condafile, err) - content, err := right.AsYaml() - pretty.Guard(err == nil, 2, "YAML error: %v", err) - holotreeBlueprint := []byte(content) - _, err = htfs.RecordEnvironment(holotreeBlueprint, false) + _, err = htfs.RecordCondaEnvironment(condafile, false) pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) } } diff --git a/common/version.go b/common/version.go index cbc4616c..aa123355 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.7` + Version = `v9.9.8` ) diff --git a/docs/changelog.md b/docs/changelog.md index f1ba8efd..83e6c1e0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.8 (date: 9.4.2021) + +- skip environment bootstrap when there is no conda.yaml used +- added index.py utility tool for generating index.html for S3 + ## v9.9.7 (date: 8.4.2021) - now `rcc holotree bootstrap` can only download templates with `--quick` @@ -39,7 +44,7 @@ - Github Actions upgrade to use Go 1.16 for rcc compilation -## v9.9.0 (date: 31.3.2021) +## v9.9.0 (date: 31.3.2021) broken - added holotree as part of source code (but not as integrated part yet) - added new internal command: holotree diff --git a/docs/index.py b/docs/index.py new file mode 100755 index 00000000..f141e954 --- /dev/null +++ b/docs/index.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import pathlib +import re +import subprocess + +LIMIT = 20 + +HEADER=''' + + + +Robocorp `rcc` downloads + + + +

Robocorp `rcc` downloads

+'''.strip() + +TESTED_HEADER=''' +

Tested versions

+

Consider these as more stable.

+'''.strip() + +LATEST_HEADER=''' +
+

Latest %(limit)d versions

+'''.strip() + + +ENTRY=''' +

%(version)s

+

Release date: %(when)s

+ +'''.strip() + +FOOTER=''' +
+ + +'''.strip() + +VERSION_PATTERN = re.compile(r'^##\s*(v[0-9.]+)\D+([0-9.]+)\D{1,5}$') +TAG_PATTERN = re.compile(r'^(v[0-9.]+)\D*$') + +DIRECTORY = pathlib.Path(__file__).parent.absolute() +CHANGELOG = DIRECTORY.joinpath('changelog.md') + +TAGLISTING = "git tag --list --sort='-taggerdate'" + +def sh(command): + task = subprocess.Popen([command], shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + out, _ = task.communicate() + return task.returncode, out.decode() + +def gittags_top(count): + code, out = sh(TAGLISTING) + if code == 0: + for line in out.splitlines(): + if count == 0: + break + if found := TAG_PATTERN.match(line): + yield(found.group(1)) + count -= 1 + +def changelog_top(count): + with open(CHANGELOG) as source: + for line in source: + if count == 0: + break + if found := VERSION_PATTERN.match(line): + yield(found.groups()) + count -= 1 + +def download(version, suffix): + return 'https://downloads.robocorp.com/rcc/releases/%s/%s' % (version, suffix) + +def process_versions(): + biglist = tuple(changelog_top(10000)) + limited = biglist[:LIMIT] if len(biglist) > LIMIT else biglist + + daymap = dict() + for version, when in biglist: + daymap[version] = when + + print(TESTED_HEADER) + for version in gittags_top(3): + details = dict(version=version, when=daymap.get(version, 'N/A')) + details['windows'] = download(version, 'windows64/rcc.exe') + details['linux'] = download(version, 'linux64/rcc') + details['macos'] = download(version, 'macos64/rcc') + print(ENTRY % details) + + print(LATEST_HEADER % dict(limit=LIMIT)) + for version, when in limited: + details = dict(version=version, when=when) + details['windows'] = download(version, 'windows64/rcc.exe') + details['linux'] = download(version, 'linux64/rcc') + details['macos'] = download(version, 'macos64/rcc') + print(ENTRY % details) + +def process(): + print(HEADER % dict(limit=LIMIT)) + process_versions() + print(FOOTER) + +if __name__ == '__main__': + process() diff --git a/htfs/commands.go b/htfs/commands.go index f8815081..1991cd09 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -10,6 +10,18 @@ import ( "github.com/robocorp/rcc/fail" ) +func RecordCondaEnvironment(condafile string, force bool) (lib Library, err error) { + defer fail.Around(&err) + + right, err := conda.ReadCondaYaml(condafile) + fail.On(err != nil, "Could not load environmet config %q, reason: %w", condafile, err) + + content, err := right.AsYaml() + fail.On(err != nil, "YAML error with %q, reason: %w", condafile, err) + + return RecordEnvironment([]byte(content), force) +} + func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { defer fail.Around(&err) From 9a5661a14c4f896e9448c5a6099e1a3059c1fec5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 9 Apr 2021 13:35:42 +0300 Subject: [PATCH 111/516] RCC-139: initial holotree integration (v9.9.9) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ docs/index.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index aa123355..dbf35add 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.8` + Version = `v9.9.9` ) diff --git a/docs/changelog.md b/docs/changelog.md index 83e6c1e0..9e3b2279 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.9 (date: 9.4.2021) + +- fixed index.py utility tool to work in correct repository + ## v9.9.8 (date: 9.4.2021) - skip environment bootstrap when there is no conda.yaml used diff --git a/docs/index.py b/docs/index.py index f141e954..862b292e 100755 --- a/docs/index.py +++ b/docs/index.py @@ -53,8 +53,9 @@ DIRECTORY = pathlib.Path(__file__).parent.absolute() CHANGELOG = DIRECTORY.joinpath('changelog.md') +REPO_ROOT = DIRECTORY.parent.absolute() -TAGLISTING = "git tag --list --sort='-taggerdate'" +TAGLISTING = f"git -C {REPO_ROOT} tag --list --sort='-taggerdate'" def sh(command): task = subprocess.Popen([command], shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) From a95683174fde1cc30724ae12239930f73fdb61ae Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 12 Apr 2021 09:58:36 +0300 Subject: [PATCH 112/516] RCC-139: initial holotree integration (v9.9.10) --- .github/workflows/rcc.yaml | 13 +++++ Rakefile | 6 +- common/version.go | 2 +- docs/changelog.md | 5 ++ docs/index.py | 117 ------------------------------------- 5 files changed, 20 insertions(+), 123 deletions(-) delete mode 100755 docs/index.py diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index f9e23653..a5f4693a 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -53,3 +53,16 @@ jobs: with: name: ${{ matrix.os }}-test-reports path: ./tmp/output/ + trigger: + name: Trigger + runs-on: ubuntu-latest + needs: + - build + - robot + steps: + - name: Pipeline + run: | + curl -X POST https://api.github.com/repos/robocorp/rcc-pipeline/dispatches \ + -H 'Accept: application/vnd.github.v3+json' \ + -u ${{ secrets.TRIGGER_TOKEN }} \ + --data '{"event_type": "pipes"}' diff --git a/Rakefile b/Rakefile index 57c5aee0..11236219 100644 --- a/Rakefile +++ b/Rakefile @@ -85,7 +85,7 @@ task :robot => :local do end desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :index_html, :linux64, :macos64, :windows64] do +task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do sh 'ls -l $(find build -type f)' end @@ -93,10 +93,6 @@ def version `sed -n -e '/Version/{s/^.*\`v//;s/\`$//p}' common/version.go`.strip end -task :index_html => :support do - sh 'docs/index.py > build/index.html' -end - task :version_txt => :support do File.write('build/version.txt', "v#{version}") end diff --git a/common/version.go b/common/version.go index dbf35add..77a714ca 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.9` + Version = `v9.9.10` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9e3b2279..bc162d95 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.10 (date: 12.4.2021) + +- removed index.py utility, since better place is on other repo, and it + was mistake to put it here + ## v9.9.9 (date: 9.4.2021) - fixed index.py utility tool to work in correct repository diff --git a/docs/index.py b/docs/index.py deleted file mode 100755 index 862b292e..00000000 --- a/docs/index.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -import pathlib -import re -import subprocess - -LIMIT = 20 - -HEADER=''' - - - -Robocorp `rcc` downloads - - - -

Robocorp `rcc` downloads

-'''.strip() - -TESTED_HEADER=''' -

Tested versions

-

Consider these as more stable.

-'''.strip() - -LATEST_HEADER=''' -
-

Latest %(limit)d versions

-'''.strip() - - -ENTRY=''' -

%(version)s

-

Release date: %(when)s

- -'''.strip() - -FOOTER=''' -
- - -'''.strip() - -VERSION_PATTERN = re.compile(r'^##\s*(v[0-9.]+)\D+([0-9.]+)\D{1,5}$') -TAG_PATTERN = re.compile(r'^(v[0-9.]+)\D*$') - -DIRECTORY = pathlib.Path(__file__).parent.absolute() -CHANGELOG = DIRECTORY.joinpath('changelog.md') -REPO_ROOT = DIRECTORY.parent.absolute() - -TAGLISTING = f"git -C {REPO_ROOT} tag --list --sort='-taggerdate'" - -def sh(command): - task = subprocess.Popen([command], shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) - out, _ = task.communicate() - return task.returncode, out.decode() - -def gittags_top(count): - code, out = sh(TAGLISTING) - if code == 0: - for line in out.splitlines(): - if count == 0: - break - if found := TAG_PATTERN.match(line): - yield(found.group(1)) - count -= 1 - -def changelog_top(count): - with open(CHANGELOG) as source: - for line in source: - if count == 0: - break - if found := VERSION_PATTERN.match(line): - yield(found.groups()) - count -= 1 - -def download(version, suffix): - return 'https://downloads.robocorp.com/rcc/releases/%s/%s' % (version, suffix) - -def process_versions(): - biglist = tuple(changelog_top(10000)) - limited = biglist[:LIMIT] if len(biglist) > LIMIT else biglist - - daymap = dict() - for version, when in biglist: - daymap[version] = when - - print(TESTED_HEADER) - for version in gittags_top(3): - details = dict(version=version, when=daymap.get(version, 'N/A')) - details['windows'] = download(version, 'windows64/rcc.exe') - details['linux'] = download(version, 'linux64/rcc') - details['macos'] = download(version, 'macos64/rcc') - print(ENTRY % details) - - print(LATEST_HEADER % dict(limit=LIMIT)) - for version, when in limited: - details = dict(version=version, when=when) - details['windows'] = download(version, 'windows64/rcc.exe') - details['linux'] = download(version, 'linux64/rcc') - details['macos'] = download(version, 'macos64/rcc') - print(ENTRY % details) - -def process(): - print(HEADER % dict(limit=LIMIT)) - process_versions() - print(FOOTER) - -if __name__ == '__main__': - process() From 11da275b222082f019c33c5596b5f634232d6acb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 13 Apr 2021 16:04:09 +0300 Subject: [PATCH 113/516] RCC-139: initial holotree integration (v9.9.11) - added support for listing holotree controller spaces --- cmd/holotreeList.go | 74 +++++++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 +++ htfs/directory.go | 11 ++++--- htfs/fs_test.go | 2 +- htfs/library.go | 45 ++++++++++++++++++++++----- 6 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 cmd/holotreeList.go diff --git a/cmd/holotreeList.go b/cmd/holotreeList.go new file mode 100644 index 00000000..a51faa9a --- /dev/null +++ b/cmd/holotreeList.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "text/tabwriter" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +func humaneHolotreeSpaceListing(tree htfs.Library) { + tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) + tabbed.Write([]byte("Identity\tController\tSpace\tBlueprint\tFull path\n")) + tabbed.Write([]byte("--------\t----------\t-----\t--------\t---------\n")) + for _, space := range tree.Spaces() { + data := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", space.Identity, space.Controller, space.Space, space.Blueprint, space.Path) + tabbed.Write([]byte(data)) + } + tabbed.Flush() +} + +func jsonicHolotreeSpaceListing(tree htfs.Library) { + details := make(map[string]map[string]string) + for _, space := range tree.Spaces() { + hold, ok := details[space.Identity] + if !ok { + hold = make(map[string]string) + details[space.Identity] = hold + hold["id"] = space.Identity + hold["controller"] = space.Controller + hold["space"] = space.Space + hold["blueprint"] = space.Blueprint + hold["path"] = space.Path + hold["meta"] = space.Path + ".meta" + hold["spec"] = filepath.Join(space.Path, "identity.yaml") + hold["plan"] = filepath.Join(space.Path, "rcc_plan.log") + } + } + body, err := json.MarshalIndent(details, "", " ") + pretty.Guard(err == nil, 1, "Could not create json, reason: %w", err) + fmt.Println(string(body)) +} + +var holotreeListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List holotree spaces.", + Long: "List holotree spaces.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree list lasted").Report() + } + + tree, err := htfs.New(common.RobocorpHome()) + pretty.Guard(err == nil, 1, "Could not get holotree, reason: %w", err) + + if jsonFlag { + jsonicHolotreeSpaceListing(tree) + } else { + humaneHolotreeSpaceListing(tree) + } + + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeListCmd) + holotreeListCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") +} diff --git a/common/version.go b/common/version.go index 77a714ca..6c75c564 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.10` + Version = `v9.9.11` ) diff --git a/docs/changelog.md b/docs/changelog.md index bc162d95..4037be2f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.11 (date: 13.4.2021) + +- added support for listing holotree controller spaces + ## v9.9.10 (date: 12.4.2021) - removed index.py utility, since better place is on other repo, and it diff --git a/htfs/directory.go b/htfs/directory.go index 869588b7..a6868bf9 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -33,10 +33,13 @@ type Dirtask func(string, *Dir) anywork.Work type Treetop func(string, *Dir) error type Root struct { - Identity string `json:"identity"` - Path string `json:"path"` - Lifted bool `json:"lifted"` - Tree *Dir `json:"tree"` + Identity string `json:"identity"` + Path string `json:"path"` + Controller string `json:"controller"` + Space string `json:"space"` + Blueprint string `json:"blueprint"` + Lifted bool `json:"lifted"` + Tree *Dir `json:"tree"` } func NewRoot(path string) (*Root, error) { diff --git a/htfs/fs_test.go b/htfs/fs_test.go index eeedec9d..46db9233 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -32,7 +32,7 @@ func TestHTFSspecification(t *testing.T) { wont.Nil(reloaded) before, err := reloaded.AsJson() must.Nil(err) - must.True(len(before) < 200) + must.True(len(before) < 300) wont.Equal(fs.Path, reloaded.Path) must.Nil(reloaded.LoadFrom(filename)) diff --git a/htfs/library.go b/htfs/library.go index 109e80c0..a46c46e7 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -39,6 +39,7 @@ func (it *stats) Dirty(dirty bool) { type Library interface { Identity() string Stage() string + Spaces() []*Root Record([]byte) error Restore([]byte, []byte, []byte) (string, error) Location(string) string @@ -50,8 +51,16 @@ type hololib struct { basedir string } +func (it *hololib) HololibDir() string { + return filepath.Join(it.basedir, "hololib") +} + +func (it *hololib) HolotreeDir() string { + return filepath.Join(it.basedir, "holotree") +} + func (it *hololib) Location(digest string) string { - return filepath.Join(it.basedir, "hololib", "library", digest[:2], digest[2:4], digest[4:6]) + return filepath.Join(it.HololibDir(), "library", digest[:2], digest[2:4], digest[4:6]) } func (it *hololib) Identity() string { @@ -59,7 +68,7 @@ func (it *hololib) Identity() string { } func (it *hololib) Stage() string { - stage := filepath.Join(it.basedir, "holotree", it.Identity()) + stage := filepath.Join(it.HolotreeDir(), it.Identity()) err := os.MkdirAll(stage, 0o755) if err != nil { panic(err) @@ -82,7 +91,8 @@ func (it *hololib) Record(blueprint []byte) error { common.Timeline("holotree (re)locator start") fs.AllFiles(Locator(it.Identity())) common.Timeline("holotree (re)locator done") - err = fs.SaveAs(filepath.Join(it.basedir, "hololib", "catalog", key)) + fs.Blueprint = key + err = fs.SaveAs(filepath.Join(it.HololibDir(), "catalog", key)) if err != nil { return err } @@ -94,7 +104,7 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - return filepath.Join(it.basedir, "hololib", "catalog", key) + return filepath.Join(it.HololibDir(), "catalog", key) } func (it *hololib) HasBlueprint(blueprint []byte) bool { @@ -102,6 +112,25 @@ func (it *hololib) HasBlueprint(blueprint []byte) bool { return pathlib.IsFile(it.CatalogPath(key)) } +func (it *hololib) Spaces() []*Root { + basedir := it.HolotreeDir() + metafiles := pathlib.Glob(basedir, "*.meta") + roots := make([]*Root, 0, len(metafiles)) + for _, metafile := range metafiles { + fullpath := filepath.Join(basedir, metafile) + root, err := NewRoot(fullpath[:len(fullpath)-5]) + if err != nil { + continue + } + err = root.LoadFrom(fullpath) + if err != nil { + continue + } + roots = append(roots, root) + } + return roots +} + func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() key := textual(sipit(blueprint), 0) @@ -109,8 +138,8 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix - metafile := filepath.Join(it.basedir, "holotree", fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(it.basedir, "holotree", name) + metafile := filepath.Join(it.HolotreeDir(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(it.HolotreeDir(), name) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { @@ -123,7 +152,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { if err != nil { return "", err } - err = fs.LoadFrom(filepath.Join(it.basedir, "hololib", "catalog", key)) + err = fs.LoadFrom(filepath.Join(it.HololibDir(), "catalog", key)) if err != nil { return "", err } @@ -139,6 +168,8 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + fs.Controller = string(client) + fs.Space = string(tag) err = fs.SaveAs(metafile) if err != nil { return "", err From 641e8f73c02ca507c8850c97a27ae936b6064988 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Apr 2021 10:12:22 +0300 Subject: [PATCH 114/516] RCC-139: initial holotree integration (v9.9.12) - updated rpaframework to version 9.5.0 in templates - added more timeline entries around holotree - minor performance related changes for holotree - removed default PYTHONPATH settings from "taskless" environment - known, remaining bug: on "env variables" command, with robot without default task and without task given in CLI, environment wont have PATH or PYTHONPATH or robot details setup correctly --- cmd/holotreeVariables.go | 2 +- common/version.go | 2 +- conda/robocorp.go | 6 ---- conda/workflows.go | 2 +- docs/changelog.md | 10 ++++++ htfs/directory.go | 11 +++--- htfs/functions.go | 66 +++++++++++++++++++++++++++------- htfs/library.go | 8 +++++ robot_tests/fullrun.robot | 2 +- templates/extended/conda.yaml | 2 +- templates/extended/tasks.robot | 1 - templates/python/conda.yaml | 2 +- templates/standard/conda.yaml | 2 +- 13 files changed, 84 insertions(+), 32 deletions(-) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 5256964c..522a31c2 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -48,7 +48,7 @@ var holotreeVariablesCmd = &cobra.Command{ ok := conda.MustMicromamba() pretty.Guard(ok, 1, "Could not get micromamba installed.") - anywork.Scale(25) + anywork.Scale(200) tree, err := htfs.RecordEnvironment(holotreeBlueprint, holotreeForce) pretty.Guard(err == nil, 2, "%w", err) diff --git a/common/version.go b/common/version.go index 6c75c564..5440eaa0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.11` + Version = `v9.9.12` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 6de58254..2f59b39b 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,7 +19,6 @@ import ( var ( ignoredPaths = []string{"python", "conda"} - pythonPaths = []string{"resources", "libraries", "tasks", "variables"} hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") randomIdentifier string ) @@ -152,10 +151,6 @@ func FindPath(environment string) pathlib.PathParts { return target } -func PythonPath() pathlib.PathParts { - return pathlib.PathFrom(pythonPaths...) -} - func EnvironmentExtensionFor(location string) []string { environment := make([]string, 0, 20) searchPath := FindPath(location) @@ -182,7 +177,6 @@ func EnvironmentExtensionFor(location string) []string { "TEMP="+RobocorpTemp(), "TMP="+RobocorpTemp(), searchPath.AsEnvironmental("PATH"), - PythonPath().AsEnvironmental("PYTHONPATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) return environment diff --git a/conda/workflows.go b/conda/workflows.go index 1c61f844..a0f61719 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -523,7 +523,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { if err != nil { return false, nil } - success := cloneFolder(source, target, 8, copier) + success := cloneFolder(source, target, 100, copier) if !success { err = removeClone(target) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index 4037be2f..69520e20 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # rcc change log +## v9.9.12 (date: 15.4.2021) + +- updated rpaframework to version 9.5.0 in templates +- added more timeline entries around holotree +- minor performance related changes for holotree +- removed default PYTHONPATH settings from "taskless" environment +- known, remaining bug: on "env variables" command, with robot without default + task and without task given in CLI, environment wont have PATH or PYTHONPATH + or robot details setup correctly + ## v9.9.11 (date: 13.4.2021) - added support for listing holotree controller spaces diff --git a/htfs/directory.go b/htfs/directory.go index a6868bf9..f4ebe7e3 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -11,6 +11,7 @@ import ( "path/filepath" "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" ) @@ -88,17 +89,20 @@ func (it *Root) Lift() error { func (it *Root) Treetop(task Treetop) error { err := task(it.Path, it.Tree) + common.Timeline("holotree treetop sync") anywork.Sync() return err } func (it *Root) AllDirs(task Dirtask) { it.Tree.AllDirs(it.Path, task) + common.Timeline("holotree dirs sync") anywork.Sync() } func (it *Root) AllFiles(task Filetask) { it.Tree.AllFiles(it.Path, task) + common.Timeline("holotree files sync") anywork.Sync() } @@ -177,12 +181,7 @@ func (it *Dir) Lift(path string) error { return err } it.Mode = stat.Mode() - source, err := os.Open(path) - if err != nil { - return err - } - defer source.Close() - content, err := source.ReadDir(-1) + content, err := os.ReadDir(path) if err != nil { return err } diff --git a/htfs/functions.go b/htfs/functions.go index 24981cf0..2d70f276 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -116,6 +116,26 @@ func LiftFile(sourcename, sinkname string) anywork.Work { } } +func LiftFlatFile(sourcename, sinkname string) anywork.Work { + return func() { + source, err := os.Open(sourcename) + if err != nil { + panic(err) + } + defer source.Close() + sink, err := os.Create(sinkname) + if err != nil { + panic(err) + } + defer sink.Close() + _, err = io.Copy(sink, source) + if err != nil { + panic(err) + } + sink.Sync() + } +} + func DropFile(sourcename, sinkname string, details *File, rewrite []byte) anywork.Work { return func() { source, err := os.Open(sourcename) @@ -154,6 +174,39 @@ func DropFile(sourcename, sinkname string, details *File, rewrite []byte) anywor } } +func DropFlatFile(sourcename, sinkname string, details *File, rewrite []byte) anywork.Work { + return func() { + source, err := os.Open(sourcename) + if err != nil { + panic(err) + } + defer source.Close() + sink, err := os.Create(sinkname) + if err != nil { + panic(err) + } + defer sink.Close() + _, err = io.Copy(sink, source) + if err != nil { + panic(err) + } + sink.Sync() + for _, position := range details.Rewrite { + _, err = sink.Seek(position, 0) + if err != nil { + panic(fmt.Sprintf("%v %d", err, position)) + } + _, err = sink.Write(rewrite) + if err != nil { + panic(err) + } + } + sink.Sync() + os.Chmod(sinkname, details.Mode) + os.Chtimes(sinkname, motherTime, motherTime) + } +} + func RemoveFile(filename string) anywork.Work { return func() { err := os.Remove(filename) @@ -175,16 +228,10 @@ func RemoveDirectory(dirname string) anywork.Work { func RestoreDirectory(library Library, fs *Root, current map[string]string, stats *stats) Dirtask { return func(path string, it *Dir) anywork.Work { return func() { - source, err := os.Open(path) - if err != nil { - panic(err) - } - content, err := source.ReadDir(-1) - source.Close() + content, err := os.ReadDir(path) if err != nil { panic(err) } - dirs := make(map[string]bool) files := make(map[string]bool) for _, part := range content { directpath := filepath.Join(path, part.Name()) @@ -193,12 +240,10 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat panic(err) } if info.IsDir() { - dirs[part.Name()] = true _, ok := it.Dirs[part.Name()] stats.Dirty(!ok) if !ok { anywork.Backlog(RemoveDirectory(directpath)) - //fmt.Println("Extra dir, remove:", directpath) } continue } @@ -207,7 +252,6 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat if !ok { stats.Dirty(true) anywork.Backlog(RemoveFile(directpath)) - //fmt.Println("Extra file, remove:", directpath) continue } shadow, ok := current[directpath] @@ -218,7 +262,6 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat directory := library.Location(found.Digest) droppath := filepath.Join(directory, found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) - //fmt.Println("Corrupted file, restore:", directpath) } } for name, found := range it.Files { @@ -229,7 +272,6 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat directory := library.Location(found.Digest) droppath := filepath.Join(directory, found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) - //fmt.Println("Missing file, restore:", directpath) } } } diff --git a/htfs/library.go b/htfs/library.go index a46c46e7..f48cf97f 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -97,7 +97,9 @@ func (it *hololib) Record(blueprint []byte) error { return err } score := &stats{} + common.Timeline("holotree lift start") err = fs.Treetop(ScheduleLifters(it, score)) + common.Timeline("holotree lift done") defer common.Timeline("- new %d/%d", score.dirty, score.total) common.Debug("Holotree new workload: %d/%d\n", score.dirty, score.total) return err @@ -146,7 +148,9 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { err = shadow.LoadFrom(metafile) } if err == nil { + common.Timeline("holotree digest start") shadow.Treetop(DigestRecorder(currentstate)) + common.Timeline("holotree digest done") } fs, err := NewRoot(it.Stage()) if err != nil { @@ -160,12 +164,16 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { if err != nil { return "", err } + common.Timeline("holotree make branches start") err = fs.Treetop(MakeBranches) + common.Timeline("holotree make branches done") if err != nil { return "", err } score := &stats{} + common.Timeline("holotree restore start") fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + common.Timeline("holotree restore done") defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) fs.Controller = string(client) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index ceb4ec05..9b83dbe4 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -125,7 +125,6 @@ Using and running template example with shell file Must Have CONDA_PROMPT_MODIFIER=(rcc) Must Have CONDA_SHLVL=1 Must Have PATH= - Must Have PYTHONPATH= Must Have PYTHONHOME= Must Have PYTHONEXECUTABLE= Must Have PYTHONNOUSERSITE=1 @@ -134,6 +133,7 @@ Using and running template example with shell file Must Have RCC_ENVIRONMENT_HASH= Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHONPATH= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= Must Have f0a9e281269b31ea diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index be963fe6..fa5adbb9 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/extended/tasks.robot b/templates/extended/tasks.robot index f1607b72..91a4374d 100644 --- a/templates/extended/tasks.robot +++ b/templates/extended/tasks.robot @@ -1,7 +1,6 @@ *** Settings *** Documentation Template robot main suite. Library Collections - Library MyLibrary Resource keywords.robot Variables MyVariables.py diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index be963fe6..fa5adbb9 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index be963fe6..fa5adbb9 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.1.0 # https://rpaframework.org/releasenotes.html + - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html From 7a21b6e33d2009301ecd153da2eb47b85e7daae6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Apr 2021 11:39:06 +0300 Subject: [PATCH 115/516] FIX: environment variables bug fix (v9.9.13) - fixing environment variables bug from previously found --- cmd/variables.go | 11 +++++++---- common/version.go | 2 +- docs/changelog.md | 4 ++++ robot_tests/fullrun.robot | 22 ++++++++++++++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cmd/variables.go b/cmd/variables.go index f107c68e..eb458059 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -58,7 +58,7 @@ func asExportedText(items []string) { } } -func exportEnvironment(condaYaml []string, packfile, taskName, environment, workspace string, validity int, jsonform bool) error { +func exportEnvironment(condaYaml []string, packfile, environment, workspace string, validity int, jsonform bool) error { var err error var config robot.Robot var task robot.Task @@ -69,7 +69,10 @@ func exportEnvironment(condaYaml []string, packfile, taskName, environment, work config, err = robot.LoadRobotYaml(packfile, false) if err == nil { condaYaml = append(condaYaml, config.CondaConfigFile()) - task = config.TaskByName(taskName) + available := config.AvailableTasks() + if len(available) > 0 { + task = config.TaskByName(available[0]) + } } } @@ -133,7 +136,7 @@ var variablesCmd = &cobra.Command{ if !ok { pretty.Exit(2, "Could not get micromamba installed.") } - err := exportEnvironment(args, robotFile, runTask, environmentFile, workspaceId, validityTime, jsonFlag) + err := exportEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, jsonFlag) if err != nil { pretty.Exit(1, "Error: Variable exporting failed because: %v", err) } @@ -145,7 +148,7 @@ func init() { variablesCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to 'env.json' development environment data file. ") variablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") - variablesCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file. ") + variablesCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file. ") variablesCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. ") variablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") variablesCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. ") diff --git a/common/version.go b/common/version.go index 5440eaa0..3179623a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.12` + Version = `v9.9.13` ) diff --git a/docs/changelog.md b/docs/changelog.md index 69520e20..f8c33ca0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.13 (date: 15.4.2021) + +- fixing environment variables bug from below + ## v9.9.12 (date: 15.4.2021) - updated rpaframework to version 9.5.0 in templates diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 9b83dbe4..6b42eb6d 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -138,6 +138,28 @@ Using and running template example with shell file Wont Have ROBOT_ARTIFACTS= Must Have f0a9e281269b31ea + Goal See variables from specific environment with robot.yaml but without task + Step build/rcc env variables --controller citests -r tmp/fluffy/robot.yaml + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Must Have PYTHONPATH= + Must Have ROBOT_ROOT= + Must Have ROBOT_ARTIFACTS= + Must Have 7b3eba72202108b9 + Goal See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml Must Be Json Response From 8b1e88512a34b0c4c0f560811b9c913741f9adb0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Apr 2021 15:43:46 +0300 Subject: [PATCH 116/516] FIX: task shell fix (v9.9.14) - environment variables conda.yaml ordering fix (from robot.yaml first) - task shell does not need task specified anymore --- README.md | 24 ++---------------------- cmd/shell.go | 4 ++-- cmd/variables.go | 6 +++++- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 13 +++++++++++++ 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3224b56d..af8cd490 100644 --- a/README.md +++ b/README.md @@ -59,29 +59,9 @@ Upgrading: `brew upgrade rcc` 1. Add to path: `sudo mv rcc /usr/local/bin/` 1. Test: `rcc` -### Direct downloads for signed executables provided by Robocorp +### [Direct downloads for signed executables provided by Robocorp](https://downloads.robocorp.com/rcc/releases/index.html) -| OS | Download URL [latest version info](https://downloads.robocorp.com/rcc/releases/latest/version.txt) | -| -------- | --------------------------------------------------------------------------------------------------- | -| Windows | https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe | -| macOS | https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc | -| Linux | https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc | - -*[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* - -## Bleeding edge (or specific versions) - -If you want to live on the edge, and do not worry about little unstability, following table gives pattern for downloading specific versions of rcc. - -Or if you want stability and live on specific version of rcc, these patterns also apply. - -And you can find versions and details from [change log](/docs/changelog.md) file. - -| OS | Download URL (replace 0.0.0 with version you want to experiment with) | -| -------- | --------------------------------------------------------------------- | -| Windows | https://downloads.robocorp.com/rcc/releases/v0.0.0/windows64/rcc.exe | -| macOS | https://downloads.robocorp.com/rcc/releases/v0.0.0/macos64/rcc | -| Linux | https://downloads.robocorp.com/rcc/releases/v0.0.0/linux64/rcc | +Follow above link to download site. Both tested and bleeding edge versions are available from same location. *[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* diff --git a/cmd/shell.go b/cmd/shell.go index b5b53b87..8c282a6b 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -25,7 +25,7 @@ command within that environment.`, if !ok { pretty.Exit(2, "Could not get micromamba installed.") } - simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) + simple, config, todo, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) if simple { pretty.Exit(1, "Cannot do shell for simple execution model.") } @@ -38,6 +38,6 @@ func init() { shellCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") shellCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") - shellCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to configure shell from configuration file.") + shellCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to configure shell from configuration file. ") shellCmd.MarkFlagRequired("config") } diff --git a/cmd/variables.go b/cmd/variables.go index eb458059..d511fbaa 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -58,13 +58,15 @@ func asExportedText(items []string) { } } -func exportEnvironment(condaYaml []string, packfile, environment, workspace string, validity int, jsonform bool) error { +func exportEnvironment(userCondaYaml []string, packfile, environment, workspace string, validity int, jsonform bool) error { var err error var config robot.Robot var task robot.Task var extra []string var data operations.Token + condaYaml := make([]string, 0, len(userCondaYaml)+2) + if Has(packfile) { config, err = robot.LoadRobotYaml(packfile, false) if err == nil { @@ -76,6 +78,8 @@ func exportEnvironment(condaYaml []string, packfile, environment, workspace stri } } + condaYaml = append(condaYaml, userCondaYaml...) + if Has(environment) { developmentEnvironment, err := robot.LoadEnvironmentSetup(environmentFile) if err == nil { diff --git a/common/version.go b/common/version.go index 3179623a..8855ad6d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.13` + Version = `v9.9.14` ) diff --git a/docs/changelog.md b/docs/changelog.md index f8c33ca0..9908c09c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.14 (date: 15.4.2021) + +- environment variables conda.yaml ordering fix (from robot.yaml first) +- task shell does not need task specified anymore + ## v9.9.13 (date: 15.4.2021) - fixing environment variables bug from below diff --git a/operations/running.go b/operations/running.go index fba1ae03..e9dfec96 100644 --- a/operations/running.go +++ b/operations/running.go @@ -45,6 +45,19 @@ func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, enviro return true } +func LoadAnyTaskEnvironment(packfile string, force bool) (bool, robot.Robot, robot.Task, string) { + FixRobot(packfile) + config, err := robot.LoadRobotYaml(packfile, true) + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + anytasks := config.AvailableTasks() + if len(anytasks) == 0 { + pretty.Exit(1, "Could not find tasks from %q.", packfile) + } + return LoadTaskWithEnvironment(packfile, anytasks[0], force) +} + func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot.Robot, robot.Task, string) { FixRobot(packfile) config, err := robot.LoadRobotYaml(packfile, true) From fe74f3328b5eaaa404585c9eea3d37a206ec3de8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 19 Apr 2021 08:17:01 +0300 Subject: [PATCH 117/516] FIX: locking problem with parallel runs (v9.9.15) - bugfix: locking while multiple rcc are doing parallel work should now work better, and not corrupt configuration (so much) --- common/version.go | 2 +- conda/cleanup.go | 1 - conda/workflows.go | 1 - docs/changelog.md | 5 +++++ operations/cache.go | 2 -- xviper/wrapper.go | 3 --- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/common/version.go b/common/version.go index 8855ad6d..9b8d7f5a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.14` + Version = `v9.9.15` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index a00a0039..55c5fb15 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -131,7 +131,6 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) err return err } defer locker.Release() - defer os.Remove(lockfile) if all { return spotlessCleanup(dryrun) diff --git a/conda/workflows.go b/conda/workflows.go index a0f61719..d0d5d414 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -343,7 +343,6 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return "", err } defer locker.Release() - defer os.Remove(lockfile) requests := xviper.GetInt("stats.env.request") + 1 hits := xviper.GetInt("stats.env.hit") diff --git a/docs/changelog.md b/docs/changelog.md index 9908c09c..b93e45eb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.15 (date: 19.4.2021) + +- bugfix: locking while multiple rcc are doing parallel work should now + work better, and not corrupt configuration (so much) + ## v9.9.14 (date: 15.4.2021) - environment variables conda.yaml ordering fix (from robot.yaml first) diff --git a/operations/cache.go b/operations/cache.go index 30b6dc43..ae4a4273 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -64,7 +64,6 @@ func SummonCache() (*Cache, error) { return nil, err } defer locker.Release() - defer os.Remove(cacheLockFile()) source, err := os.Open(cacheLocation()) if err != nil { @@ -85,7 +84,6 @@ func (it *Cache) Save() error { return err } defer locker.Release() - defer os.Remove(cacheLockFile()) sink, err := os.Create(cacheLocation()) if err != nil { diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 93aa3b19..c443f099 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -2,7 +2,6 @@ package xviper import ( "fmt" - "os" "time" "github.com/robocorp/rcc/common" @@ -45,7 +44,6 @@ func (it *config) Save() { return } defer locker.Release() - defer os.Remove(it.Lockfile) err = it.Viper.WriteConfigAs(it.Filename) if err != nil { @@ -65,7 +63,6 @@ func (it *config) Reload() { return } defer locker.Release() - defer os.Remove(it.Lockfile) it.Viper = viper.New() it.Viper.SetConfigFile(it.Filename) From bf1eacf52e302090167d4783d702cadc4ab231cb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 20 Apr 2021 09:16:06 +0300 Subject: [PATCH 118/516] RCC-139: initial holotree integration (v9.9.16) - added support for deleting holotree controller spaces - added holotree and hololib to full environment cleanup - added required parameter to `rcc env delete` command also --- cmd/delete.go | 3 ++- cmd/holotreeDelete.go | 33 +++++++++++++++++++++++++ cmd/holotreeList.go | 15 +++++------- common/variables.go | 12 +++++++-- common/version.go | 2 +- conda/cleanup.go | 4 +++ docs/changelog.md | 6 +++++ htfs/commands.go | 29 +++++++++++++++++++++- htfs/library.go | 57 ++++++++++++++++++------------------------- 9 files changed, 114 insertions(+), 47 deletions(-) create mode 100644 cmd/holotreeDelete.go diff --git a/cmd/delete.go b/cmd/delete.go index 08529dce..2aad1171 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -9,10 +9,11 @@ import ( ) var deleteCmd = &cobra.Command{ - Use: "delete", + Use: "delete +", Short: "Delete one managed virtual environment.", Long: `Delete the given virtual environment from existence. After deletion, it will not be available anymore.`, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { for _, prefix := range args { for _, label := range conda.FindEnvironment(prefix) { diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go new file mode 100644 index 00000000..44545c19 --- /dev/null +++ b/cmd/holotreeDelete.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var holotreeDeleteCmd = &cobra.Command{ + Use: "delete +", + Short: "Delete holotree controller space.", + Long: "Delete holotree controller space.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + for _, prefix := range args { + for _, label := range htfs.FindEnvironment(prefix) { + common.Log("Removing %v", label) + if dryFlag { + continue + } + err := htfs.RemoveHolotreeSpace(label) + pretty.Guard(err == nil, 1, "Error: %v", err) + } + } + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeDeleteCmd) + holotreeDeleteCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") +} diff --git a/cmd/holotreeList.go b/cmd/holotreeList.go index a51faa9a..5e354e77 100644 --- a/cmd/holotreeList.go +++ b/cmd/holotreeList.go @@ -13,20 +13,20 @@ import ( "github.com/spf13/cobra" ) -func humaneHolotreeSpaceListing(tree htfs.Library) { +func humaneHolotreeSpaceListing() { tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) tabbed.Write([]byte("Identity\tController\tSpace\tBlueprint\tFull path\n")) tabbed.Write([]byte("--------\t----------\t-----\t--------\t---------\n")) - for _, space := range tree.Spaces() { + for _, space := range htfs.Spaces() { data := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", space.Identity, space.Controller, space.Space, space.Blueprint, space.Path) tabbed.Write([]byte(data)) } tabbed.Flush() } -func jsonicHolotreeSpaceListing(tree htfs.Library) { +func jsonicHolotreeSpaceListing() { details := make(map[string]map[string]string) - for _, space := range tree.Spaces() { + for _, space := range htfs.Spaces() { hold, ok := details[space.Identity] if !ok { hold = make(map[string]string) @@ -56,13 +56,10 @@ var holotreeListCmd = &cobra.Command{ defer common.Stopwatch("Holotree list lasted").Report() } - tree, err := htfs.New(common.RobocorpHome()) - pretty.Guard(err == nil, 1, "Could not get holotree, reason: %w", err) - if jsonFlag { - jsonicHolotreeSpaceListing(tree) + jsonicHolotreeSpaceListing() } else { - humaneHolotreeSpaceListing(tree) + humaneHolotreeSpaceListing() } }, diff --git a/common/variables.go b/common/variables.go index cc4ccc7c..91b41241 100644 --- a/common/variables.go +++ b/common/variables.go @@ -37,9 +37,9 @@ func init() { func RobocorpHome() string { home := os.Getenv(ROBOCORP_HOME_VARIABLE) if len(home) > 0 { - return ExpandPath(home) + return ensureDirectory(ExpandPath(home)) } - return ExpandPath(defaultRobocorpLocation) + return ensureDirectory(ExpandPath(defaultRobocorpLocation)) } func ensureDirectory(name string) string { @@ -63,6 +63,14 @@ func BaseLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "base")) } +func HololibLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "hololib")) +} + +func HolotreeLocation() string { + return ensureDirectory(filepath.Join(RobocorpHome(), "holotree")) +} + func PipCache() string { return ensureDirectory(filepath.Join(RobocorpHome(), "pipcache")) } diff --git a/common/version.go b/common/version.go index 9b8d7f5a..d166f2f3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.15` + Version = `v9.9.16` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 55c5fb15..c62eea9b 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -66,12 +66,16 @@ func spotlessCleanup(dryrun bool) error { common.Log("Would be removing:") common.Log("- %v", common.BaseLocation()) common.Log("- %v", common.LiveLocation()) + common.Log("- %v", common.HolotreeLocation()) + common.Log("- %v", common.HololibLocation()) common.Log("- %v", common.PipCache()) common.Log("- %v", MambaPackages()) common.Log("- %v", BinMicromamba()) common.Log("- %v", RobocorpTempRoot()) return nil } + safeRemove("cache", common.HolotreeLocation()) + safeRemove("cache", common.HololibLocation()) safeRemove("cache", common.BaseLocation()) safeRemove("cache", common.LiveLocation()) safeRemove("cache", common.PipCache()) diff --git a/docs/changelog.md b/docs/changelog.md index b93e45eb..2b665de2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.9.16 (date: 20.4.2021) + +- added support for deleting holotree controller spaces +- added holotree and hololib to full environment cleanup +- added required parameter to `rcc env delete` command also + ## v9.9.15 (date: 19.4.2021) - bugfix: locking while multiple rcc are doing parallel work should now diff --git a/htfs/commands.go b/htfs/commands.go index 1991cd09..6ff239ce 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" @@ -25,7 +26,7 @@ func RecordCondaEnvironment(condafile string, force bool) (lib Library, err erro func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { defer fail.Around(&err) - tree, err := New(common.RobocorpHome()) + tree, err := New() fail.On(err != nil, "Failed to create holotree location, reason %w.", err) // following must be setup here @@ -55,3 +56,29 @@ func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { } return tree, nil } + +func FindEnvironment(fragment string) []string { + result := make([]string, 0, 10) + for directory, _ := range Spacemap() { + name := filepath.Base(directory) + if strings.Contains(name, fragment) { + result = append(result, name) + } + } + return result +} + +func RemoveHolotreeSpace(label string) (err error) { + defer fail.Around(&err) + + for directory, metafile := range Spacemap() { + name := filepath.Base(directory) + if name != label { + continue + } + os.Remove(metafile) + err = os.RemoveAll(directory) + fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) + } + return nil +} diff --git a/htfs/library.go b/htfs/library.go index f48cf97f..23f885ce 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -39,7 +39,6 @@ func (it *stats) Dirty(dirty bool) { type Library interface { Identity() string Stage() string - Spaces() []*Root Record([]byte) error Restore([]byte, []byte, []byte) (string, error) Location(string) string @@ -51,16 +50,8 @@ type hololib struct { basedir string } -func (it *hololib) HololibDir() string { - return filepath.Join(it.basedir, "hololib") -} - -func (it *hololib) HolotreeDir() string { - return filepath.Join(it.basedir, "holotree") -} - func (it *hololib) Location(digest string) string { - return filepath.Join(it.HololibDir(), "library", digest[:2], digest[2:4], digest[4:6]) + return filepath.Join(common.HololibLocation(), "library", digest[:2], digest[2:4], digest[4:6]) } func (it *hololib) Identity() string { @@ -68,7 +59,7 @@ func (it *hololib) Identity() string { } func (it *hololib) Stage() string { - stage := filepath.Join(it.HolotreeDir(), it.Identity()) + stage := filepath.Join(common.HolotreeLocation(), it.Identity()) err := os.MkdirAll(stage, 0o755) if err != nil { panic(err) @@ -92,7 +83,7 @@ func (it *hololib) Record(blueprint []byte) error { fs.AllFiles(Locator(it.Identity())) common.Timeline("holotree (re)locator done") fs.Blueprint = key - err = fs.SaveAs(filepath.Join(it.HololibDir(), "catalog", key)) + err = fs.SaveAs(filepath.Join(common.HololibLocation(), "catalog", key)) if err != nil { return err } @@ -106,7 +97,7 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - return filepath.Join(it.HololibDir(), "catalog", key) + return filepath.Join(common.HololibLocation(), "catalog", key) } func (it *hololib) HasBlueprint(blueprint []byte) bool { @@ -114,17 +105,24 @@ func (it *hololib) HasBlueprint(blueprint []byte) bool { return pathlib.IsFile(it.CatalogPath(key)) } -func (it *hololib) Spaces() []*Root { - basedir := it.HolotreeDir() - metafiles := pathlib.Glob(basedir, "*.meta") - roots := make([]*Root, 0, len(metafiles)) - for _, metafile := range metafiles { +func Spacemap() map[string]string { + result := make(map[string]string) + basedir := common.HolotreeLocation() + for _, metafile := range pathlib.Glob(basedir, "*.meta") { fullpath := filepath.Join(basedir, metafile) - root, err := NewRoot(fullpath[:len(fullpath)-5]) + result[fullpath[:len(fullpath)-5]] = fullpath + } + return result +} + +func Spaces() []*Root { + roots := make([]*Root, 0, 20) + for directory, metafile := range Spacemap() { + root, err := NewRoot(directory) if err != nil { continue } - err = root.LoadFrom(fullpath) + err = root.LoadFrom(metafile) if err != nil { continue } @@ -140,8 +138,8 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix - metafile := filepath.Join(it.HolotreeDir(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(it.HolotreeDir(), name) + metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(common.HolotreeLocation(), name) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { @@ -156,7 +154,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { if err != nil { return "", err } - err = fs.LoadFrom(filepath.Join(it.HololibDir(), "catalog", key)) + err = fs.LoadFrom(filepath.Join(common.HololibLocation(), "catalog", key)) if err != nil { return "", err } @@ -208,19 +206,12 @@ func makedirs(prefix string, suffixes ...string) error { return nil } -func New(location string) (Library, error) { - basedir, err := filepath.Abs(location) - if err != nil { - return nil, err - } - err = makedirs(basedir, "hololib", "holotree") - if err != nil { - return nil, err - } - err = makedirs(filepath.Join(basedir, "hololib"), "library", "catalog") +func New() (Library, error) { + err := makedirs(common.HololibLocation(), "library", "catalog") if err != nil { return nil, err } + basedir := common.RobocorpHome() return &hololib{ identity: sipit([]byte(basedir)), basedir: basedir, From 436ef8dfd804649e94225c67162ec3c6eca0f28f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 20 Apr 2021 17:57:53 +0300 Subject: [PATCH 119/516] RCC-139: initial holotree integration (v9.9.17) - added environment, workspace, and robot support to holotree variables command - also added some robot tests for holotree to verify functionality --- cmd/commontools.go | 4 ++ cmd/holotreeVariables.go | 127 +++++++++++++++++++++++++++---------- cmd/variables.go | 27 ++------ common/version.go | 2 +- docs/changelog.md | 5 ++ robot_tests/holotree.robot | 89 ++++++++++++++++++++++++++ 6 files changed, 198 insertions(+), 56 deletions(-) create mode 100644 robot_tests/holotree.robot diff --git a/cmd/commontools.go b/cmd/commontools.go index 24919de2..f8e0e81d 100644 --- a/cmd/commontools.go +++ b/cmd/commontools.go @@ -8,6 +8,10 @@ const ( environmentAccount = `RCC_CREDENTIALS_ID` ) +func Has(value string) bool { + return len(value) > 0 +} + func AccountName() string { if len(accountName) > 0 { return accountName diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 522a31c2..e79706d6 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -1,11 +1,15 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" "github.com/spf13/cobra" ) @@ -16,47 +20,100 @@ var ( holotreeJson bool ) +func robotBlueprints(userBlueprints []string, packfile string) (robot.Robot, []string) { + var err error + var config robot.Robot + + blueprints := make([]string, 0, len(userBlueprints)+2) + + if Has(packfile) { + config, err = robot.LoadRobotYaml(packfile, false) + if err == nil { + blueprints = append(blueprints, config.CondaConfigFile()) + } + } + + return config, append(blueprints, userBlueprints...) +} + +func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, space string, force bool) []string { + var left, right *conda.Environment + var err error + var extra []string + var data operations.Token + + config, filenames := robotBlueprints(userFiles, packfile) + + if Has(environment) { + developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) + if err == nil { + extra = developmentEnvironment.AsEnvironment() + } + } + + for _, filename := range filenames { + left = right + right, err = conda.ReadCondaYaml(filename) + pretty.Guard(err == nil, 2, "Failure: %v", err) + if left == nil { + continue + } + right, err = left.Merge(right) + pretty.Guard(err == nil, 3, "Failure: %v", err) + } + pretty.Guard(right != nil, 4, "Missing environment specification(s).") + content, err := right.AsYaml() + pretty.Guard(err == nil, 5, "YAML error: %v", err) + holotreeBlueprint = []byte(content) + + anywork.Scale(200) + + tree, err := htfs.RecordEnvironment(holotreeBlueprint, force) + pretty.Guard(err == nil, 6, "%w", err) + + path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(space)) + pretty.Guard(err == nil, 7, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + + env := conda.EnvironmentExtensionFor(path) + if config != nil { + env = config.ExecutionEnvironment(path, extra, false) + } + + if Has(workspace) { + claims := operations.RunClaims(validity*60, workspace) + data, err = operations.AuthorizeClaims(AccountName(), claims) + pretty.Guard(err == nil, 8, "Failed to get cloud data, reason: %v", err) + } + + if len(data) > 0 { + endpoint := data["endpoint"] + for _, key := range rcHosts { + env = append(env, fmt.Sprintf("%s=%s", key, endpoint)) + } + token := data["token"] + for _, key := range rcTokens { + env = append(env, fmt.Sprintf("%s=%s", key, token)) + } + env = append(env, fmt.Sprintf("RC_WORKSPACE_ID=%s", workspaceId)) + } + + return env +} + var holotreeVariablesCmd = &cobra.Command{ Use: "variables conda.yaml+", Aliases: []string{"vars"}, Short: "Do holotree operations.", Long: "Do holotree operations.", - Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { - defer common.Stopwatch("Holotree command lasted").Report() - } - - var left, right *conda.Environment - var err error - - for _, filename := range args { - left = right - right, err = conda.ReadCondaYaml(filename) - pretty.Guard(err == nil, 1, "Failure: %v", err) - if left == nil { - continue - } - right, err = left.Merge(right) - pretty.Guard(err == nil, 1, "Failure: %v", err) + defer common.Stopwatch("Holotree variables command lasted").Report() } - pretty.Guard(right != nil, 1, "Missing environment specification(s).") - content, err := right.AsYaml() - pretty.Guard(err == nil, 1, "YAML error: %v", err) - holotreeBlueprint = []byte(content) ok := conda.MustMicromamba() pretty.Guard(ok, 1, "Could not get micromamba installed.") - anywork.Scale(200) - - tree, err := htfs.RecordEnvironment(holotreeBlueprint, holotreeForce) - pretty.Guard(err == nil, 2, "%w", err) - - path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(holotreeSpace)) - pretty.Guard(err == nil, 10, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) - - env := conda.EnvironmentExtensionFor(path) + env := holotreeExpandEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, holotreeSpace, holotreeForce) if holotreeJson { asJson(env) } else { @@ -67,8 +124,14 @@ var holotreeVariablesCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeVariablesCmd) - holotreeVariablesCmd.Flags().StringVar(&holotreeSpace, "space", "", "Client specific name to identify this environment.") + holotreeVariablesCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to 'env.json' development environment data file. ") + holotreeVariablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") + holotreeVariablesCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. ") + holotreeVariablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") + holotreeVariablesCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for workspace. ") + + holotreeVariablesCmd.Flags().StringVarP(&holotreeSpace, "space", "s", "", "Client specific name to identify this environment.") holotreeVariablesCmd.MarkFlagRequired("space") - holotreeVariablesCmd.Flags().BoolVar(&holotreeForce, "force", false, "Force environment creation with refresh.") - holotreeVariablesCmd.Flags().BoolVar(&holotreeJson, "json", false, "Show environment as JSON.") + holotreeVariablesCmd.Flags().BoolVarP(&holotreeForce, "force", "f", false, "Force environment creation with refresh.") + holotreeVariablesCmd.Flags().BoolVarP(&holotreeJson, "json", "j", false, "Show environment as JSON.") } diff --git a/cmd/variables.go b/cmd/variables.go index d511fbaa..dd98a019 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -14,10 +14,6 @@ import ( "github.com/spf13/cobra" ) -func Has(value string) bool { - return len(value) > 0 -} - func asSimpleMap(line string) map[string]string { parts := strings.SplitN(strings.TrimSpace(line), "=", 2) if len(parts) != 2 { @@ -60,28 +56,13 @@ func asExportedText(items []string) { func exportEnvironment(userCondaYaml []string, packfile, environment, workspace string, validity int, jsonform bool) error { var err error - var config robot.Robot - var task robot.Task var extra []string var data operations.Token - condaYaml := make([]string, 0, len(userCondaYaml)+2) - - if Has(packfile) { - config, err = robot.LoadRobotYaml(packfile, false) - if err == nil { - condaYaml = append(condaYaml, config.CondaConfigFile()) - available := config.AvailableTasks() - if len(available) > 0 { - task = config.TaskByName(available[0]) - } - } - } - - condaYaml = append(condaYaml, userCondaYaml...) + config, condaYaml := robotBlueprints(userCondaYaml, packfile) if Has(environment) { - developmentEnvironment, err := robot.LoadEnvironmentSetup(environmentFile) + developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) if err == nil { extra = developmentEnvironment.AsEnvironment() } @@ -97,8 +78,8 @@ func exportEnvironment(userCondaYaml []string, packfile, environment, workspace } env := conda.EnvironmentExtensionFor(label) - if task != nil { - env = task.ExecutionEnvironment(config, label, extra, false) + if config != nil { + env = config.ExecutionEnvironment(label, extra, false) } if Has(workspace) { diff --git a/common/version.go b/common/version.go index d166f2f3..6cf09a04 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.16` + Version = `v9.9.17` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2b665de2..cc4bf9e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.17 (date: 20.4.2021) + +- added environment, workspace, and robot support to holotree variables command +- also added some robot tests for holotree to verify functionality + ## v9.9.16 (date: 20.4.2021) - added support for deleting holotree controller spaces diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot new file mode 100644 index 00000000..e97d9fdf --- /dev/null +++ b/robot_tests/holotree.robot @@ -0,0 +1,89 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot + +*** Test cases *** + +Holotree testing flow + + Goal Initialize new standard robot into tmp/holotin folder using force. + Step build/rcc robot init --controller citests -t extended -d tmp/holotin -f + Use STDERR + Must Have OK. + + Goal See variables from specific environment without robot.yaml knowledge + Step build/rcc holotree variables --space jam --controller citests conda/testdata/conda.yaml + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHONPATH= + Wont Have ROBOT_ROOT= + Wont Have ROBOT_ARTIFACTS= + + Goal See variables from specific environment with robot.yaml but without task + Step build/rcc holotree variables --space jam --controller citests -r tmp/holotin/robot.yaml + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Must Have PYTHONPATH= + Must Have ROBOT_ROOT= + Must Have ROBOT_ARTIFACTS= + + Goal See variables from specific environment without robot.yaml knowledge in JSON form + Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml + Must Be Json Response + + Goal See variables from specific environment with robot.yaml knowledge + Step build/rcc holotree variables --space jam --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONPATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Must Have ROBOT_ROOT= + Must Have ROBOT_ARTIFACTS= + Wont Have RC_API_SECRET_HOST= + Wont Have RC_API_WORKITEM_HOST= + Wont Have RC_API_SECRET_TOKEN= + Wont Have RC_API_WORKITEM_TOKEN= + Wont Have RC_WORKSPACE_ID= + + Goal See variables from specific environment with robot.yaml knowledge in JSON form + Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json + Must Be Json Response From 3ac44e829217647cba64273ac75628509d2753d2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 28 Apr 2021 09:16:24 +0300 Subject: [PATCH 120/516] RCC-139: initial holotree integration (v9.9.18) - some cleanup on code base - changed autoupdate url for Robocorp Lab --- assets/settings.yaml | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 14 +++++++------- robot/robot.go | 30 ------------------------------ 5 files changed, 14 insertions(+), 39 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index c6cb1138..413a5bc7 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -19,7 +19,7 @@ diagnostics-hosts: autoupdates: assistant: https://downloads.robocorp.com/assistant/releases/ workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ - lab: https://downloads.code.robocorp.com/lab/installer/ + lab: https://downloads.robocorp.com/lab/releases/ certificates: verify-ssl: true diff --git a/common/version.go b/common/version.go index 6cf09a04..0bbbfe8e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.17` + Version = `v9.9.18` ) diff --git a/docs/changelog.md b/docs/changelog.md index cc4bf9e1..cea147d1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.18 (date: 28.4.2021) + +- some cleanup on code base +- changed autoupdate url for Robocorp Lab + ## v9.9.17 (date: 20.4.2021) - added environment, workspace, and robot support to holotree variables command diff --git a/operations/running.go b/operations/running.go index e9dfec96..dcb73f3d 100644 --- a/operations/running.go +++ b/operations/running.go @@ -101,7 +101,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t task := make([]string, len(template)) copy(task, template) searchPath := pathlib.TargetPath() - searchPath = searchPath.Prepend(todo.Paths(config)...) + searchPath = searchPath.Prepend(config.Paths()...) found, ok := searchPath.Which(task[0], conda.FileExtensions) if !ok { pretty.Exit(6, "Error: Cannot find command: %v", task[0]) @@ -119,7 +119,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t pretty.Exit(8, "Error: %v", err) } task[0] = fullpath - directory := todo.WorkingDirectory(config) + directory := config.WorkingDirectory() environment := robot.PlainEnvironment([]string{searchPath.AsEnvironmental("PATH")}, true) if len(data) > 0 { endpoint := data["endpoint"] @@ -137,7 +137,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t environment = append(environment, fmt.Sprintf("%s=%s", key, value)) } } - outputDir := todo.ArtifactDirectory(config) + outputDir := config.ArtifactDirectory() common.Debug("DEBUG: about to run command - %v", task) _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) if err != nil { @@ -154,7 +154,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } task := make([]string, len(template)) copy(task, template) - searchPath := todo.SearchPath(config, label) + searchPath := config.SearchPath(label) found, ok := searchPath.Which(task[0], conda.FileExtensions) if !ok { pretty.Exit(6, "Error: Cannot find command: %v", task[0]) @@ -172,8 +172,8 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro pretty.Exit(8, "Error: %v", err) } task[0] = fullpath - directory := todo.WorkingDirectory(config) - environment := todo.ExecutionEnvironment(config, label, developmentEnvironment.AsEnvironment(), true) + directory := config.WorkingDirectory() + environment := config.ExecutionEnvironment(label, developmentEnvironment.AsEnvironment(), true) if len(data) > 0 { endpoint := data["endpoint"] for _, key := range rcHosts { @@ -192,7 +192,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } before := make(map[string]string) beforeHash, beforeErr := conda.DigestFor(label, before) - outputDir := todo.ArtifactDirectory(config) + outputDir := config.ArtifactDirectory() if !flags.Assistant && !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } diff --git a/robot/robot.go b/robot/robot.go index dc499125..14aeaf4a 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -39,12 +39,6 @@ type Robot interface { } type Task interface { - WorkingDirectory(Robot) string - ArtifactDirectory(Robot) string - Paths(Robot) pathlib.PathParts - PythonPaths(Robot) pathlib.PathParts - SearchPath(Robot, string) pathlib.PathParts - ExecutionEnvironment(robot Robot, location string, inject []string, full bool) []string Commandline() []string } @@ -356,30 +350,6 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo return environment } -func (it *task) WorkingDirectory(robot Robot) string { - return robot.WorkingDirectory() -} - -func (it *task) ArtifactDirectory(robot Robot) string { - return robot.ArtifactDirectory() -} - -func (it *task) SearchPath(robot Robot, location string) pathlib.PathParts { - return robot.SearchPath(location) -} - -func (it *task) Paths(robot Robot) pathlib.PathParts { - return robot.Paths() -} - -func (it *task) PythonPaths(robot Robot) pathlib.PathParts { - return robot.PythonPaths() -} - -func (it *task) ExecutionEnvironment(robot Robot, location string, inject []string, full bool) []string { - return robot.ExecutionEnvironment(location, inject, full) -} - func (it *task) shellCommand() []string { result, err := shlex.Split(it.Shell) if err != nil { From a0bc450a905348d0a3408f55aaad1dc34f4bcc92 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 29 Apr 2021 14:18:07 +0300 Subject: [PATCH 121/516] RCC-139: initial holotree integration (v9.9.19) --- cmd/holotreeBootstrap.go | 4 +- cmd/holotreeVariables.go | 14 +++-- common/version.go | 2 +- docs/changelog.md | 6 ++ htfs/commands.go | 17 +++-- htfs/functions.go | 29 +++++++-- htfs/library.go | 114 ++++++++++++++++++++++++++++++++++ robot_tests/certificates.yaml | 4 ++ robot_tests/holotree.robot | 42 +++++++++++++ 9 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 robot_tests/certificates.yaml diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 17467ed5..09e828c2 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -34,7 +34,9 @@ func updateEnvironments(robots []string) { continue } condafile := config.CondaConfigFile() - _, err = htfs.RecordCondaEnvironment(condafile, false) + tree, err := htfs.New() + pretty.Guard(err == nil, 2, "Holotree creation error: %v", err) + err = htfs.RecordCondaEnvironment(tree, condafile, false) pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) } } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index e79706d6..63359011 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -68,11 +68,17 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp anywork.Scale(200) - tree, err := htfs.RecordEnvironment(holotreeBlueprint, force) - pretty.Guard(err == nil, 6, "%w", err) + tree, err := htfs.New() + pretty.Guard(err == nil, 6, "%s", err) + + if !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { + tree = htfs.Virtual() + } + err = htfs.RecordEnvironment(tree, holotreeBlueprint, force) + pretty.Guard(err == nil, 7, "%s", err) path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(space)) - pretty.Guard(err == nil, 7, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + pretty.Guard(err == nil, 8, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) env := conda.EnvironmentExtensionFor(path) if config != nil { @@ -82,7 +88,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp if Has(workspace) { claims := operations.RunClaims(validity*60, workspace) data, err = operations.AuthorizeClaims(AccountName(), claims) - pretty.Guard(err == nil, 8, "Failed to get cloud data, reason: %v", err) + pretty.Guard(err == nil, 9, "Failed to get cloud data, reason: %v", err) } if len(data) > 0 { diff --git a/common/version.go b/common/version.go index 0bbbfe8e..2994db34 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.18` + Version = `v9.9.19` ) diff --git a/docs/changelog.md b/docs/changelog.md index cea147d1..68976497 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.9.19 (date: 29.4.2021) + +- refactoring to enable virtual holotree for --liveonly functionality +- NOTE: leased environments functionality will go away when holotree + goes mainstream (and plan for that is rcc series v10) + ## v9.9.18 (date: 28.4.2021) - some cleanup on code base diff --git a/htfs/commands.go b/htfs/commands.go index 6ff239ce..f0a27d69 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -11,7 +11,7 @@ import ( "github.com/robocorp/rcc/fail" ) -func RecordCondaEnvironment(condafile string, force bool) (lib Library, err error) { +func RecordCondaEnvironment(tree Library, condafile string, force bool) (err error) { defer fail.Around(&err) right, err := conda.ReadCondaYaml(condafile) @@ -20,22 +20,21 @@ func RecordCondaEnvironment(condafile string, force bool) (lib Library, err erro content, err := right.AsYaml() fail.On(err != nil, "YAML error with %q, reason: %w", condafile, err) - return RecordEnvironment([]byte(content), force) + return RecordEnvironment(tree, []byte(content), force) } -func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { +func RecordEnvironment(tree Library, blueprint []byte, force bool) (err error) { defer fail.Around(&err) - tree, err := New() - fail.On(err != nil, "Failed to create holotree location, reason %w.", err) - // following must be setup here common.StageFolder = tree.Stage() common.Stageonly = true - common.Liveonly = true err = os.RemoveAll(tree.Stage()) - fail.On(err != nil, "Failed to clean stage, reason %w.", err) + fail.On(err != nil, "Failed to clean stage, reason %v.", err) + + err = os.MkdirAll(tree.Stage(), 0o755) + fail.On(err != nil, "Failed to create stage, reason %v.", err) common.Debug("Holotree stage is %q.", tree.Stage()) exists := tree.HasBlueprint(blueprint) @@ -54,7 +53,7 @@ func RecordEnvironment(blueprint []byte, force bool) (lib Library, err error) { err := tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } - return tree, nil + return nil } func FindEnvironment(fragment string) []string { diff --git a/htfs/functions.go b/htfs/functions.go index 2d70f276..7db312d1 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -13,6 +13,20 @@ import ( "github.com/robocorp/rcc/trollhash" ) +func DigestMapper(target map[string]string) Treetop { + var tool Treetop + tool = func(path string, it *Dir) error { + for name, subdir := range it.Dirs { + tool(filepath.Join(path, name), subdir) + } + for name, file := range it.Files { + target[file.Digest] = filepath.Join(path, name) + } + return nil + } + return tool +} + func DigestRecorder(target map[string]string) Treetop { var tool Treetop tool = func(path string, it *Dir) error { @@ -143,9 +157,14 @@ func DropFile(sourcename, sinkname string, details *File, rewrite []byte) anywor panic(err) } defer source.Close() - reader, err := gzip.NewReader(source) + var reader io.ReadCloser + reader, err = gzip.NewReader(source) if err != nil { - panic(err) + _, err = source.Seek(0, 0) + if err != nil { + panic(err) + } + reader = source } defer reader.Close() sink, err := os.Create(sinkname) @@ -259,8 +278,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat ok = golden && found.Match(info) stats.Dirty(!ok) if !ok { - directory := library.Location(found.Digest) - droppath := filepath.Join(directory, found.Digest) + droppath := library.ExactLocation(found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) } } @@ -269,8 +287,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat _, seen := files[name] if !seen { stats.Dirty(true) - directory := library.Location(found.Digest) - droppath := filepath.Join(directory, found.Digest) + droppath := library.ExactLocation(found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) } } diff --git a/htfs/library.go b/htfs/library.go index 23f885ce..88891f0d 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -42,6 +42,7 @@ type Library interface { Record([]byte) error Restore([]byte, []byte, []byte) (string, error) Location(string) string + ExactLocation(string) string HasBlueprint([]byte) bool } @@ -54,6 +55,10 @@ func (it *hololib) Location(digest string) string { return filepath.Join(common.HololibLocation(), "library", digest[:2], digest[2:4], digest[4:6]) } +func (it *hololib) ExactLocation(digest string) string { + return filepath.Join(common.HololibLocation(), "library", digest[:2], digest[2:4], digest[4:6], digest) +} + func (it *hololib) Identity() string { return fmt.Sprintf("h%016xt", it.identity) } @@ -196,6 +201,9 @@ func textual(key uint64, size int) string { } func makedirs(prefix string, suffixes ...string) error { + if common.Liveonly { + return nil + } for _, suffix := range suffixes { fullpath := filepath.Join(prefix, suffix) err := os.MkdirAll(fullpath, 0o755) @@ -217,3 +225,109 @@ func New() (Library, error) { basedir: basedir, }, nil } + +type virtual struct { + identity uint64 + root *Root + registry map[string]string + key string +} + +func Virtual() Library { + return &virtual{ + identity: sipit([]byte(common.RobocorpHome())), + } +} + +func (it *virtual) Identity() string { + return fmt.Sprintf("v%016xh", it.identity) +} + +func (it *virtual) Stage() string { + stage := filepath.Join(common.HolotreeLocation(), it.Identity()) + err := os.MkdirAll(stage, 0o755) + if err != nil { + panic(err) + } + return stage +} + +func (it *virtual) Record(blueprint []byte) error { + defer common.Stopwatch("Holotree recording took:").Debug() + key := textual(sipit(blueprint), 0) + common.Timeline("holotree record start %s (virtual)", key) + fs, err := NewRoot(it.Stage()) + if err != nil { + return err + } + err = fs.Lift() + if err != nil { + return err + } + common.Timeline("holotree (re)locator start (virtual)") + fs.AllFiles(Locator(it.Identity())) + common.Timeline("holotree (re)locator done (virtual)") + it.registry = make(map[string]string) + fs.Treetop(DigestMapper(it.registry)) + fs.Blueprint = key + it.root = fs + it.key = key + return nil +} + +func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { + defer common.Stopwatch("Holotree restore took:").Debug() + key := textual(sipit(blueprint), 0) + common.Timeline("holotree restore start %s (virtual)", key) + prefix := textual(sipit(client), 9) + suffix := textual(sipit(tag), 8) + name := prefix + "_" + suffix + metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(common.HolotreeLocation(), name) + currentstate := make(map[string]string) + shadow, err := NewRoot(targetdir) + if err == nil { + err = shadow.LoadFrom(metafile) + } + if err == nil { + common.Timeline("holotree digest start (virtual)") + shadow.Treetop(DigestRecorder(currentstate)) + common.Timeline("holotree digest done (virtual)") + } + fs := it.root + err = fs.Relocate(targetdir) + if err != nil { + return "", err + } + common.Timeline("holotree make branches start (virtual)") + err = fs.Treetop(MakeBranches) + common.Timeline("holotree make branches done (virtual)") + if err != nil { + return "", err + } + score := &stats{} + common.Timeline("holotree restore start (virtual)") + fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + common.Timeline("holotree restore done (virtual)") + defer common.Timeline("- dirty %d/%d", score.dirty, score.total) + common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + fs.Controller = string(client) + fs.Space = string(tag) + err = fs.SaveAs(metafile) + if err != nil { + return "", err + } + return targetdir, nil +} + +func (it *virtual) ExactLocation(key string) string { + return it.registry[key] +} + +func (it *virtual) Location(key string) string { + panic("Location is not supported on virtual holotree.") +} + +func (it *virtual) HasBlueprint(blueprint []byte) bool { + return it.key == textual(sipit(blueprint), 0) +} diff --git a/robot_tests/certificates.yaml b/robot_tests/certificates.yaml new file mode 100644 index 00000000..9c46ef58 --- /dev/null +++ b/robot_tests/certificates.yaml @@ -0,0 +1,4 @@ +channels: + - conda-forge +dependencies: + - ca-certificates diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index e97d9fdf..375b5471 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -83,7 +83,49 @@ Holotree testing flow Wont Have RC_API_SECRET_TOKEN= Wont Have RC_API_WORKITEM_TOKEN= Wont Have RC_WORKSPACE_ID= + Use STDERR + Wont Have (virtual) + Wont Have live only Goal See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json Must Be Json Response + Use STDERR + Wont Have (virtual) + Wont Have live only + + Goal Liveonly works and uses virtual holotree + Step build/rcc holotree vars --liveonly --space jam --controller citests robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHONPATH= + Wont Have ROBOT_ROOT= + Wont Have ROBOT_ARTIFACTS= + Wont Have RC_API_SECRET_HOST= + Wont Have RC_API_WORKITEM_HOST= + Wont Have RC_API_SECRET_TOKEN= + Wont Have RC_API_WORKITEM_TOKEN= + Wont Have RC_WORKSPACE_ID= + Use STDERR + Must Have (virtual) + Must Have live only + + Goal Liveonly works and uses virtual holotree and can give output in JSON form + Step build/rcc ht vars --liveonly --space jam --controller citests --json robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline + Must Be Json Response + Use STDERR + Must Have (virtual) + Must Have live only From 08a294625a1b347b462041a5c60c48dd53bde398 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 3 May 2021 08:49:04 +0300 Subject: [PATCH 122/516] RCC-139: initial holotree integration (v9.9.20) - added blueprint subcommand to holotree hierarchy to query blueprint existence in hololib --- cmd/holotreeBlueprints.go | 52 +++++++++++++++++++++++++++++++++++++++ cmd/holotreeVariables.go | 50 +++++++------------------------------ cmd/variables.go | 3 ++- common/version.go | 2 +- docs/changelog.md | 5 ++++ htfs/commands.go | 40 ++++++++++++++++++++++++++++++ htfs/library.go | 17 +++++++------ 7 files changed, 119 insertions(+), 50 deletions(-) create mode 100644 cmd/holotreeBlueprints.go diff --git a/cmd/holotreeBlueprints.go b/cmd/holotreeBlueprints.go new file mode 100644 index 00000000..f7baa3b7 --- /dev/null +++ b/cmd/holotreeBlueprints.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +func holotreeExpandBlueprint(userFiles []string, packfile string) map[string]interface{} { + result := make(map[string]interface{}) + + _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) + pretty.Guard(err == nil, 5, "%s", err) + + tree, err := htfs.New() + pretty.Guard(err == nil, 6, "%s", err) + + result["hash"] = htfs.BlueprintHash(holotreeBlueprint) + result["exist"] = tree.HasBlueprint(holotreeBlueprint) + + return result +} + +var holotreeBlueprintCmd = &cobra.Command{ + Use: "blueprint conda.yaml+", + Short: "Verify that resulting blueprint is in hololibrary.", + Long: "Verify that resulting blueprint is in hololibrary.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree blueprints command lasted").Report() + } + + status := holotreeExpandBlueprint(args, robotFile) + if holotreeJson { + out, err := operations.NiceJsonOutput(status) + pretty.Guard(err == nil, 6, "%s", err) + fmt.Println(out) + } else { + common.Log("Blueprint %q is available: %v", status["hash"], status["exist"]) + } + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeBlueprintCmd) + holotreeBlueprintCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") + holotreeBlueprintCmd.Flags().BoolVarP(&holotreeJson, "json", "j", false, "Show environment as JSON.") +} diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 63359011..538e9ebd 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -20,51 +20,12 @@ var ( holotreeJson bool ) -func robotBlueprints(userBlueprints []string, packfile string) (robot.Robot, []string) { - var err error - var config robot.Robot - - blueprints := make([]string, 0, len(userBlueprints)+2) - - if Has(packfile) { - config, err = robot.LoadRobotYaml(packfile, false) - if err == nil { - blueprints = append(blueprints, config.CondaConfigFile()) - } - } - - return config, append(blueprints, userBlueprints...) -} - func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, space string, force bool) []string { - var left, right *conda.Environment - var err error var extra []string var data operations.Token - config, filenames := robotBlueprints(userFiles, packfile) - - if Has(environment) { - developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) - if err == nil { - extra = developmentEnvironment.AsEnvironment() - } - } - - for _, filename := range filenames { - left = right - right, err = conda.ReadCondaYaml(filename) - pretty.Guard(err == nil, 2, "Failure: %v", err) - if left == nil { - continue - } - right, err = left.Merge(right) - pretty.Guard(err == nil, 3, "Failure: %v", err) - } - pretty.Guard(right != nil, 4, "Missing environment specification(s).") - content, err := right.AsYaml() - pretty.Guard(err == nil, 5, "YAML error: %v", err) - holotreeBlueprint = []byte(content) + config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) + pretty.Guard(err == nil, 5, "%s", err) anywork.Scale(200) @@ -80,6 +41,13 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(space)) pretty.Guard(err == nil, 8, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + if Has(environment) { + developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) + if err == nil { + extra = developmentEnvironment.AsEnvironment() + } + } + env := conda.EnvironmentExtensionFor(path) if config != nil { env = config.ExecutionEnvironment(path, extra, false) diff --git a/cmd/variables.go b/cmd/variables.go index dd98a019..7c8ed008 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -7,6 +7,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -59,7 +60,7 @@ func exportEnvironment(userCondaYaml []string, packfile, environment, workspace var extra []string var data operations.Token - config, condaYaml := robotBlueprints(userCondaYaml, packfile) + config, condaYaml := htfs.RobotBlueprints(userCondaYaml, packfile) if Has(environment) { developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) diff --git a/common/version.go b/common/version.go index 2994db34..3b08b047 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.19` + Version = `v9.9.20` ) diff --git a/docs/changelog.md b/docs/changelog.md index 68976497..5474fa05 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.9.20 (date: 3.5.2021) + +- added blueprint subcommand to holotree hierarchy to query blueprint + existence in hololib + ## v9.9.19 (date: 29.4.2021) - refactoring to enable virtual holotree for --liveonly functionality diff --git a/htfs/commands.go b/htfs/commands.go index f0a27d69..5482a305 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/robot" ) func RecordCondaEnvironment(tree Library, condafile string, force bool) (err error) { @@ -81,3 +82,42 @@ func RemoveHolotreeSpace(label string) (err error) { } return nil } + +func RobotBlueprints(userBlueprints []string, packfile string) (robot.Robot, []string) { + var err error + var config robot.Robot + + blueprints := make([]string, 0, len(userBlueprints)+2) + + if len(packfile) > 0 { + config, err = robot.LoadRobotYaml(packfile, false) + if err == nil { + blueprints = append(blueprints, config.CondaConfigFile()) + } + } + + return config, append(blueprints, userBlueprints...) +} + +func ComposeFinalBlueprint(userFiles []string, packfile string) (config robot.Robot, blueprint []byte, err error) { + defer fail.Around(&err) + + var left, right *conda.Environment + + config, filenames := RobotBlueprints(userFiles, packfile) + + for _, filename := range filenames { + left = right + right, err = conda.ReadCondaYaml(filename) + fail.On(err != nil, "Failure: %v", err) + if left == nil { + continue + } + right, err = left.Merge(right) + fail.On(err != nil, "Failure: %v", err) + } + fail.On(right == nil, "Missing environment specification(s).") + content, err := right.AsYaml() + fail.On(err != nil, "YAML error: %v", err) + return config, []byte(content), nil +} diff --git a/htfs/library.go b/htfs/library.go index 88891f0d..41e56239 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -74,7 +74,7 @@ func (it *hololib) Stage() string { func (it *hololib) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() - key := textual(sipit(blueprint), 0) + key := BlueprintHash(blueprint) common.Timeline("holotree record start %s", key) fs, err := NewRoot(it.Stage()) if err != nil { @@ -106,8 +106,7 @@ func (it *hololib) CatalogPath(key string) string { } func (it *hololib) HasBlueprint(blueprint []byte) bool { - key := textual(sipit(blueprint), 0) - return pathlib.IsFile(it.CatalogPath(key)) + return pathlib.IsFile(it.CatalogPath(BlueprintHash(blueprint))) } func Spacemap() map[string]string { @@ -138,7 +137,7 @@ func Spaces() []*Root { func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() - key := textual(sipit(blueprint), 0) + key := BlueprintHash(blueprint) common.Timeline("holotree restore start %s", key) prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) @@ -188,6 +187,10 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { return targetdir, nil } +func BlueprintHash(blueprint []byte) string { + return textual(sipit(blueprint), 0) +} + func sipit(key []byte) uint64 { return siphash.Hash(9007199254740993, 2147483647, key) } @@ -254,7 +257,7 @@ func (it *virtual) Stage() string { func (it *virtual) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() - key := textual(sipit(blueprint), 0) + key := BlueprintHash(blueprint) common.Timeline("holotree record start %s (virtual)", key) fs, err := NewRoot(it.Stage()) if err != nil { @@ -277,7 +280,7 @@ func (it *virtual) Record(blueprint []byte) error { func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() - key := textual(sipit(blueprint), 0) + key := BlueprintHash(blueprint) common.Timeline("holotree restore start %s (virtual)", key) prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) @@ -329,5 +332,5 @@ func (it *virtual) Location(key string) string { } func (it *virtual) HasBlueprint(blueprint []byte) bool { - return it.key == textual(sipit(blueprint), 0) + return it.key == BlueprintHash(blueprint) } From 54ac20d19d3cdd70f7c904d1a9595288089818f8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 4 May 2021 09:12:55 +0300 Subject: [PATCH 123/516] FIX: configuration flag documentation (v9.9.21) - documentation fix for toplevel config flag, closes #18 --- cmd/root.go | 2 +- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 2b1598a4..631db868 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,7 +92,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&profilefile, "pprof", "", "Filename to save profiling information.") rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP/rcc.yaml)") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") diff --git a/common/version.go b/common/version.go index 3b08b047..cf0cc21c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.20` + Version = `v9.9.21` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5474fa05..9243acfe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.9.21 (date: 4.5.2021) + +- documentation fix for toplevel config flag, closes #18 + ## v9.9.20 (date: 3.5.2021) - added blueprint subcommand to holotree hierarchy to query blueprint From 4a31caeb1e95a8d5df18c7ca2f66d2c4683463cc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 4 May 2021 12:12:43 +0300 Subject: [PATCH 124/516] RCC-139: initial holotree integration (v9.10.0) - refactoring code so that runs can be converted to holotree - added `--space` option to runs so that they can use holotree - holotree blueprint should now be unified form (same hash everywhere) - holotree now co-exists with old implementation in backward compatible way --- cmd/assistantRun.go | 1 + cmd/holotreeBlueprints.go | 9 ++++++--- cmd/holotreeVariables.go | 25 +++++++++---------------- cmd/run.go | 1 + cmd/testrun.go | 1 + common/variables.go | 1 + common/version.go | 2 +- conda/workflows.go | 4 ++-- docs/changelog.md | 7 +++++++ htfs/commands.go | 26 +++++++++++++++++++++++++- htfs/library.go | 5 +++-- operations/running.go | 8 +++++++- robot_tests/holotree.robot | 2 +- 13 files changed, 65 insertions(+), 27 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 1f39b2c6..74b536e7 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -130,4 +130,5 @@ func init() { assistantRunCmd.MarkFlagRequired("assistant") assistantRunCmd.Flags().StringVarP(©Directory, "copy", "c", "", "Location to copy changed artifacts from run (optional).") assistantRunCmd.Flags().BoolVarP(&useEcc, "ecc", "", false, "DO NOT USE! INTERNAL EXPERIMENT!") + assistantRunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") } diff --git a/cmd/holotreeBlueprints.go b/cmd/holotreeBlueprints.go index f7baa3b7..c2a47da4 100644 --- a/cmd/holotreeBlueprints.go +++ b/cmd/holotreeBlueprints.go @@ -16,6 +16,8 @@ func holotreeExpandBlueprint(userFiles []string, packfile string) map[string]int _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) + common.Debug("FINAL blueprint:\n%s", string(holotreeBlueprint)) + tree, err := htfs.New() pretty.Guard(err == nil, 6, "%s", err) @@ -26,9 +28,10 @@ func holotreeExpandBlueprint(userFiles []string, packfile string) map[string]int } var holotreeBlueprintCmd = &cobra.Command{ - Use: "blueprint conda.yaml+", - Short: "Verify that resulting blueprint is in hololibrary.", - Long: "Verify that resulting blueprint is in hololibrary.", + Use: "blueprint conda.yaml+", + Short: "Verify that resulting blueprint is in hololibrary.", + Long: "Verify that resulting blueprint is in hololibrary.", + Aliases: []string{"bp"}, Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Holotree blueprints command lasted").Report() diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 538e9ebd..fcd38c7b 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -2,8 +2,9 @@ package cmd import ( "fmt" + "os" + "path/filepath" - "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" @@ -15,31 +16,23 @@ import ( var ( holotreeBlueprint []byte - holotreeSpace string holotreeForce bool holotreeJson bool ) -func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, space string, force bool) []string { +func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, force bool) []string { var extra []string var data operations.Token config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) - anywork.Scale(200) - - tree, err := htfs.New() + condafile := filepath.Join(conda.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) + err = os.WriteFile(condafile, holotreeBlueprint, 0o640) pretty.Guard(err == nil, 6, "%s", err) - if !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { - tree = htfs.Virtual() - } - err = htfs.RecordEnvironment(tree, holotreeBlueprint, force) - pretty.Guard(err == nil, 7, "%s", err) - - path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(space)) - pretty.Guard(err == nil, 8, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + path, err := htfs.NewEnvironment(force, condafile) + pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) @@ -87,7 +80,7 @@ var holotreeVariablesCmd = &cobra.Command{ ok := conda.MustMicromamba() pretty.Guard(ok, 1, "Could not get micromamba installed.") - env := holotreeExpandEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, holotreeSpace, holotreeForce) + env := holotreeExpandEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, holotreeForce) if holotreeJson { asJson(env) } else { @@ -104,7 +97,7 @@ func init() { holotreeVariablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") holotreeVariablesCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for workspace. ") - holotreeVariablesCmd.Flags().StringVarP(&holotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + holotreeVariablesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") holotreeVariablesCmd.MarkFlagRequired("space") holotreeVariablesCmd.Flags().BoolVarP(&holotreeForce, "force", "f", false, "Force environment creation with refresh.") holotreeVariablesCmd.Flags().BoolVarP(&holotreeJson, "json", "j", false, "Show environment as JSON.") diff --git a/cmd/run.go b/cmd/run.go index 5fd045b1..bad9ada8 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -62,4 +62,5 @@ func init() { runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in teminal/command prompt. For development only, not for production!") + runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") } diff --git a/cmd/testrun.go b/cmd/testrun.go index e4b06755..bc0c8e19 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -92,4 +92,5 @@ func init() { testrunCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") testrunCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") testrunCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") + testrunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") } diff --git a/common/variables.go b/common/variables.go index 91b41241..0c245268 100644 --- a/common/variables.go +++ b/common/variables.go @@ -22,6 +22,7 @@ var ( LeaseEffective bool StageFolder string ControllerType string + HolotreeSpace string LeaseContract string EnvironmentHash string SemanticTag string diff --git a/common/version.go b/common/version.go index cf0cc21c..a71266f9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.9.21` + Version = `v9.10.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index d0d5d414..dbe9ea4d 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -379,8 +379,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } defer os.Remove(condaYaml) defer os.Remove(requirementsText) - common.Log("#### Progress: 1/6 [environment key is: %s]", key) - common.Timeline("1/6 key %s.", key) + common.Log("#### Progress: 1/6 [environment key is: %s (deprecated)]", key) + common.Timeline("1/6 key %s (deprecated).", key) common.EnvironmentHash = key diff --git a/docs/changelog.md b/docs/changelog.md index 9243acfe..7fa5f402 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.10.0 (date: 4.5.2021) + +- refactoring code so that runs can be converted to holotree +- added `--space` option to runs so that they can use holotree +- holotree blueprint should now be unified form (same hash everywhere) +- holotree now co-exists with old implementation in backward compatible way + ## v9.9.21 (date: 4.5.2021) - documentation fix for toplevel config flag, closes #18 diff --git a/htfs/commands.go b/htfs/commands.go index 5482a305..0efefc06 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -6,12 +6,35 @@ import ( "path/filepath" "strings" + "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/robot" ) +func NewEnvironment(force bool, condafile string) (label string, err error) { + defer fail.Around(&err) + + _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") + fail.On(err != nil, "%s", err) + + anywork.Scale(200) + + tree, err := New() + fail.On(err != nil, "%s", err) + + if !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { + tree = Virtual() + } + err = RecordEnvironment(tree, holotreeBlueprint, force) + fail.On(err != nil, "%s", err) + + path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + return path, nil +} + func RecordCondaEnvironment(tree Library, condafile string, force bool) (err error) { defer fail.Around(&err) @@ -30,6 +53,7 @@ func RecordEnvironment(tree Library, blueprint []byte, force bool) (err error) { // following must be setup here common.StageFolder = tree.Stage() common.Stageonly = true + common.Liveonly = true err = os.RemoveAll(tree.Stage()) fail.On(err != nil, "Failed to clean stage, reason %v.", err) @@ -119,5 +143,5 @@ func ComposeFinalBlueprint(userFiles []string, packfile string) (config robot.Ro fail.On(right == nil, "Missing environment specification(s).") content, err := right.AsYaml() fail.On(err != nil, "YAML error: %v", err) - return config, []byte(content), nil + return config, []byte(strings.TrimSpace(content)), nil } diff --git a/htfs/library.go b/htfs/library.go index 41e56239..52f42743 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -88,12 +88,13 @@ func (it *hololib) Record(blueprint []byte) error { fs.AllFiles(Locator(it.Identity())) common.Timeline("holotree (re)locator done") fs.Blueprint = key - err = fs.SaveAs(filepath.Join(common.HololibLocation(), "catalog", key)) + catalog := filepath.Join(common.HololibLocation(), "catalog", key) + err = fs.SaveAs(catalog) if err != nil { return err } score := &stats{} - common.Timeline("holotree lift start") + common.Timeline("holotree lift start %q", catalog) err = fs.Treetop(ScheduleLifters(it, score)) common.Timeline("holotree lift done") defer common.Timeline("- new %d/%d", score.dirty, score.total) diff --git a/operations/running.go b/operations/running.go index dcb73f3d..e4827d4f 100644 --- a/operations/running.go +++ b/operations/running.go @@ -7,6 +7,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -79,7 +80,12 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. return true, config, todo, "" } - label, err := conda.NewEnvironment(force, config.CondaConfigFile()) + var label string + if len(common.HolotreeSpace) > 0 { + label, err = htfs.NewEnvironment(force, config.CondaConfigFile()) + } else { + label, err = conda.NewEnvironment(force, config.CondaConfigFile()) + } if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 375b5471..63c28cfe 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -85,7 +85,7 @@ Holotree testing flow Wont Have RC_WORKSPACE_ID= Use STDERR Wont Have (virtual) - Wont Have live only + Must Have live only Goal See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json From 338279d4de585d75080c905921f8c6742c2b93f4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 5 May 2021 13:33:47 +0300 Subject: [PATCH 125/516] RCC-168: holotree pre-check for components (v9.10.1) - added check for all components owned by catalog, to verify that they all are actually there - added debug level logging on environment restoration operations - added possibility to have line numbers on rcc produced log output (stderr) - rcc log output (stderr) is now synchronized thru a channel - made holotree command tree visible on toplevel listing --- cmd/holotree.go | 1 - cmd/rcc/main.go | 3 +++ cmd/root.go | 1 + common/logger.go | 53 +++++++++++++++++++++++++++++++++++++-------- common/variables.go | 1 + common/version.go | 2 +- docs/changelog.md | 9 ++++++++ htfs/functions.go | 30 +++++++++++++++++++++++-- htfs/library.go | 21 +++++++++++++++++- 9 files changed, 107 insertions(+), 14 deletions(-) diff --git a/cmd/holotree.go b/cmd/holotree.go index 2161c60b..32821b3b 100644 --- a/cmd/holotree.go +++ b/cmd/holotree.go @@ -9,7 +9,6 @@ var holotreeCmd = &cobra.Command{ Aliases: []string{"ht"}, Short: "Group of holotree commands.", Long: "Group of holotree commands.", - Hidden: true, } func init() { diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 92659559..d870a1d5 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -25,13 +25,16 @@ func ExitProtection() { if ok { exit.ShowMessage() cloud.WaitTelemetry() + common.WaitLogs() os.Exit(exit.Code) } cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) cloud.WaitTelemetry() + common.WaitLogs() panic(status) } cloud.WaitTelemetry() + common.WaitLogs() } func startTempRecycling() { diff --git a/cmd/root.go b/cmd/root.go index 631db868..763b93e7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -100,6 +100,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&pretty.Colorless, "colorless", "", false, "do not use colors in CLI UI") rootCmd.PersistentFlags().BoolVarP(&common.NoCache, "nocache", "", false, "do not use cache for credentials and tokens, always request them from cloud") + rootCmd.PersistentFlags().BoolVarP(&common.LogLinenumbers, "numbers", "", false, "put line numbers on rcc produced log output") rootCmd.PersistentFlags().BoolVarP(&common.DebugFlag, "debug", "", false, "to get debug output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TraceFlag, "trace", "", false, "to get trace output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") diff --git a/common/logger.go b/common/logger.go index a6ebd268..7ae78106 100644 --- a/common/logger.go +++ b/common/logger.go @@ -2,23 +2,57 @@ package common import ( "fmt" - "io" "os" + "sync" "time" ) -func printout(out io.Writer, message string) { +var ( + logsource = make(logwriters) + logbarrier = sync.WaitGroup{} +) + +type logwriter func() (*os.File, string) +type logwriters chan logwriter + +func loggerLoop(writers logwriters) { var stamp string - if TraceFlag { - stamp = time.Now().Format("02.150405.000 ") + line := uint64(0) + for { + line += 1 + todo, ok := <-writers + if !ok { + continue + } + out, message := todo() + + if TraceFlag { + stamp = time.Now().Format("02.150405.000 ") + } else if LogLinenumbers { + stamp = fmt.Sprintf("%3d ", line) + } else { + stamp = "" + } + fmt.Fprintf(out, "%s%s\n", stamp, message) + out.Sync() + logbarrier.Done() + } +} + +func init() { + go loggerLoop(logsource) +} + +func printout(out *os.File, message string) { + logbarrier.Add(1) + logsource <- func() (*os.File, string) { + return out, message } - fmt.Fprintf(out, "%s%s\n", stamp, message) } func Fatal(context string, err error) { if err != nil { printout(os.Stderr, fmt.Sprintf("Fatal [%s]: %v", context, err)) - os.Stderr.Sync() } } @@ -31,14 +65,12 @@ func Error(context string, err error) { func Log(format string, details ...interface{}) { if !Silent { printout(os.Stderr, fmt.Sprintf(format, details...)) - os.Stderr.Sync() } } func Debug(format string, details ...interface{}) error { if DebugFlag { printout(os.Stderr, fmt.Sprintf(format, details...)) - os.Stderr.Sync() } return nil } @@ -46,7 +78,6 @@ func Debug(format string, details ...interface{}) error { func Trace(format string, details ...interface{}) error { if TraceFlag { printout(os.Stderr, fmt.Sprintf(format, details...)) - os.Stderr.Sync() } return nil } @@ -55,3 +86,7 @@ func Stdout(format string, details ...interface{}) { fmt.Fprintf(os.Stdout, format, details...) os.Stdout.Sync() } + +func WaitLogs() { + logbarrier.Wait() +} diff --git a/common/variables.go b/common/variables.go index 0c245268..a38a6d36 100644 --- a/common/variables.go +++ b/common/variables.go @@ -16,6 +16,7 @@ var ( Silent bool DebugFlag bool TraceFlag bool + LogLinenumbers bool NoCache bool Liveonly bool Stageonly bool diff --git a/common/version.go b/common/version.go index a71266f9..fa51213f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.10.0` + Version = `v9.10.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7fa5f402..bc01f058 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v9.10.1 (date: 5.5.2021) + +- added check for all components owned by catalog, to verify that they all + are actually there +- added debug level logging on environment restoration operations +- added possibility to have line numbers on rcc produced log output (stderr) +- rcc log output (stderr) is now synchronized thru a channel +- made holotree command tree visible on toplevel listing + ## v9.10.0 (date: 4.5.2021) - refactoring code so that runs can be converted to holotree diff --git a/htfs/functions.go b/htfs/functions.go index 7db312d1..85b3de6f 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -9,10 +9,32 @@ import ( "path/filepath" "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/trollhash" ) +func CatalogCheck(library Library, fs *Root) Treetop { + var tool Treetop + tool = func(path string, it *Dir) error { + for name, file := range it.Files { + location := library.ExactLocation(file.Digest) + if !pathlib.IsFile(location) { + fullpath := filepath.Join(path, name) + return fmt.Errorf("Content for %q [%s] is missing!", fullpath, file.Digest) + } + } + for name, subdir := range it.Dirs { + err := tool(filepath.Join(path, name), subdir) + if err != nil { + return err + } + } + return nil + } + return tool +} + func DigestMapper(target map[string]string) Treetop { var tool Treetop tool = func(path string, it *Dir) error { @@ -260,17 +282,19 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat } if info.IsDir() { _, ok := it.Dirs[part.Name()] - stats.Dirty(!ok) if !ok { + common.Debug("* Holotree: remove extra directory %q", directpath) anywork.Backlog(RemoveDirectory(directpath)) } + stats.Dirty(!ok) continue } files[part.Name()] = true found, ok := it.Files[part.Name()] if !ok { - stats.Dirty(true) + common.Debug("* Holotree: remove extra file %q", directpath) anywork.Backlog(RemoveFile(directpath)) + stats.Dirty(true) continue } shadow, ok := current[directpath] @@ -278,6 +302,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat ok = golden && found.Match(info) stats.Dirty(!ok) if !ok { + common.Debug("* Holotree: update changed file %q", directpath) droppath := library.ExactLocation(found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) } @@ -287,6 +312,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat _, seen := files[name] if !seen { stats.Dirty(true) + common.Debug("* Holotree: add missing file %q", directpath) droppath := library.ExactLocation(found.Digest) anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) } diff --git a/htfs/library.go b/htfs/library.go index 52f42743..977d920f 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -9,6 +9,7 @@ import ( "github.com/dchest/siphash" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" ) @@ -107,7 +108,25 @@ func (it *hololib) CatalogPath(key string) string { } func (it *hololib) HasBlueprint(blueprint []byte) bool { - return pathlib.IsFile(it.CatalogPath(BlueprintHash(blueprint))) + catalog := it.CatalogPath(BlueprintHash(blueprint)) + tempdir := filepath.Join(conda.RobocorpTemp(), BlueprintHash(blueprint)) + shadow, err := NewRoot(tempdir) + if err != nil { + return false + } + err = shadow.LoadFrom(catalog) + if err != nil { + common.Debug("Catalog load failed, reason: %v", err) + return false + } + common.Timeline("holotree content check start") + err = shadow.Treetop(CatalogCheck(it, shadow)) + common.Timeline("holotree content check done") + if err != nil { + common.Debug("Catalog check failed, reason: %v", err) + return false + } + return pathlib.IsFile(catalog) } func Spacemap() map[string]string { From 58d2fe489d64a9df03cc1721a3a553d453d9b4a9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 5 May 2021 16:42:47 +0300 Subject: [PATCH 126/516] RCC-168: holotree pre-check for components (v9.10.2) - added metrics to see when there was catalog failure (pre-check related) - added PYTHONDONTWRITEBYTECODE=x setting into rcc generated environments, since this will pollute the cache (every compilation produces different file) without much of benefits - also added PYTHONPYCACHEPREFIX to point into temporary folder - added `--space` flag to `rcc cloud prepare` command --- cmd/cloudPrepare.go | 9 ++++++++- common/version.go | 2 +- conda/robocorp.go | 2 ++ docs/changelog.md | 9 +++++++++ htfs/library.go | 5 +++++ robot/robot.go | 2 ++ 6 files changed, 27 insertions(+), 2 deletions(-) diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index de993786..0ef58b64 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -53,8 +54,13 @@ var prepareCloudCmd = &cobra.Command{ pretty.Guard(err == nil, 7, "Error: %v", err) pretty.Guard(config.UsesConda(), 0, "Ok.") + var label string condafile := config.CondaConfigFile() - label, err := conda.NewEnvironment(false, condafile) + if len(common.HolotreeSpace) > 0 { + label, err = htfs.NewEnvironment(false, condafile) + } else { + label, err = conda.NewEnvironment(false, condafile) + } pretty.Guard(err == nil, 8, "Error: %v", err) common.Log("Prepared %q.", label) @@ -68,4 +74,5 @@ func init() { prepareCloudCmd.MarkFlagRequired("workspace") prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") prepareCloudCmd.MarkFlagRequired("robot") + prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") } diff --git a/common/version.go b/common/version.go index fa51213f..94202697 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.10.1` + Version = `v9.10.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 2f59b39b..4bc4c301 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -170,6 +170,8 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", + "PYTHONDONTWRITEBYTECODE=x", + "PYTHONPYCACHEPREFIX="+RobocorpTemp(), "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), diff --git a/docs/changelog.md b/docs/changelog.md index bc01f058..3039a7f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v9.10.2 (date: 5.5.2021) + +- added metrics to see when there was catalog failure (pre-check related) +- added PYTHONDONTWRITEBYTECODE=x setting into rcc generated environments, + since this will pollute the cache (every compilation produces different file) + without much of benefits +- also added PYTHONPYCACHEPREFIX to point into temporary folder +- added `--space` flag to `rcc cloud prepare` command + ## v9.10.1 (date: 5.5.2021) - added check for all components owned by catalog, to verify that they all diff --git a/htfs/library.go b/htfs/library.go index 977d920f..98cb9826 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -8,6 +8,7 @@ import ( "time" "github.com/dchest/siphash" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" @@ -109,6 +110,9 @@ func (it *hololib) CatalogPath(key string) string { func (it *hololib) HasBlueprint(blueprint []byte) bool { catalog := it.CatalogPath(BlueprintHash(blueprint)) + if !pathlib.IsFile(catalog) { + return false + } tempdir := filepath.Join(conda.RobocorpTemp(), BlueprintHash(blueprint)) shadow, err := NewRoot(tempdir) if err != nil { @@ -123,6 +127,7 @@ func (it *hololib) HasBlueprint(blueprint []byte) bool { err = shadow.Treetop(CatalogCheck(it, shadow)) common.Timeline("holotree content check done") if err != nil { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.holotree.catalog.failure", common.Version) common.Debug("Catalog check failed, reason: %v", err) return false } diff --git a/robot/robot.go b/robot/robot.go index 14aeaf4a..d52ffd33 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -335,6 +335,8 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", + "PYTHONDONTWRITEBYTECODE=x", + "PYTHONPYCACHEPREFIX="+conda.RobocorpTemp(), "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), From f4ed0fb80acd5a0f6950b4f7cc913caff2432324 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 6 May 2021 12:14:44 +0300 Subject: [PATCH 127/516] RCC-173: use new authorization scheme (v9.11.0) - started using new capabilitySet feature of cloud authorization - added metric for run/robot authorization usage - one minor typo fix with "terminal" word --- cmd/authorize.go | 4 +-- cmd/holotreeVariables.go | 2 +- cmd/run.go | 2 +- cmd/variables.go | 2 +- common/version.go | 2 +- docs/changelog.md | 6 ++++ operations/authorize.go | 65 ++++++++++++------------------------ operations/authorize_test.go | 40 +++++++--------------- operations/running.go | 4 +-- operations/updownload.go | 4 +-- operations/workspaces.go | 4 +-- 11 files changed, 52 insertions(+), 83 deletions(-) diff --git a/cmd/authorize.go b/cmd/authorize.go index 4aec1c73..50ddaab0 100644 --- a/cmd/authorize.go +++ b/cmd/authorize.go @@ -20,9 +20,9 @@ var authorizeCmd = &cobra.Command{ } var claims *operations.Claims if granularity == "user" { - claims = operations.WorkspaceTreeClaims(validityTime * 60) + claims = operations.ViewWorkspacesClaims(validityTime * 60) } else { - claims = operations.RunClaims(validityTime*60, workspaceId) + claims = operations.RunRobotClaims(validityTime*60, workspaceId) } data, err := operations.AuthorizeClaims(AccountName(), claims) if err != nil { diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index fcd38c7b..62b76922 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -47,7 +47,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp } if Has(workspace) { - claims := operations.RunClaims(validity*60, workspace) + claims := operations.RunRobotClaims(validity*60, workspace) data, err = operations.AuthorizeClaims(AccountName(), claims) pretty.Guard(err == nil, 9, "Failed to get cloud data, reason: %v", err) } diff --git a/cmd/run.go b/cmd/run.go index bad9ada8..c56f0890 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -61,6 +61,6 @@ func init() { runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") - runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in teminal/command prompt. For development only, not for production!") + runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") } diff --git a/cmd/variables.go b/cmd/variables.go index 7c8ed008..3de827cc 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -84,7 +84,7 @@ func exportEnvironment(userCondaYaml []string, packfile, environment, workspace } if Has(workspace) { - claims := operations.RunClaims(validity*60, workspace) + claims := operations.RunRobotClaims(validity*60, workspace) data, err = operations.AuthorizeClaims(AccountName(), claims) } diff --git a/common/version.go b/common/version.go index 94202697..0b7d8aa9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.10.2` + Version = `v9.11.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3039a7f8..1cb7ece5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.11.0 (date: 6.5.2021) + +- started using new capabilitySet feature of cloud authorization +- added metric for run/robot authorization usage +- one minor typo fix with "terminal" word + ## v9.10.2 (date: 5.5.2021) - added metrics to see when there was catalog failure (pre-check related) diff --git a/operations/authorize.go b/operations/authorize.go index df164406..a994084f 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -29,15 +29,12 @@ const ( newline = "\n" ) -type Capability map[string]bool -type Capabilities map[string]Capability - type Claims struct { - ExpiresIn int `json:"expiresIn,omitempty"` - Capabilities Capabilities `json:"capabilities,omitempty"` - Method string `json:"-"` - Url string `json:"-"` - Name string `json:"-"` + ExpiresIn int `json:"expiresIn,omitempty"` + CapabilitySet string `json:"capabilitySet,omitempty"` + Method string `json:"-"` + Url string `json:"-"` + Name string `json:"-"` } type Token map[string]interface{} @@ -65,11 +62,10 @@ type UserInfo struct { func NewClaims(name, url string, expires int) *Claims { result := Claims{ - ExpiresIn: expires, - Capabilities: make(Capabilities), - Url: url, - Name: name, - Method: postMethod, + ExpiresIn: expires, + Url: url, + Name: name, + Method: postMethod, } return &result } @@ -96,45 +92,28 @@ func (it *Claims) AsJson() (string, error) { return string(body), nil } -func (it Capabilities) Add(name string, list, read, write bool) { - capability := make(Capability) - capability["list"] = list - capability["read"] = read - capability["write"] = write - it[name] = capability -} - -func ActivityClaims(seconds int, workspace string) *Claims { - result := NewClaims("Activity", fmt.Sprintf(WorkspaceApi, workspace), seconds) - result.Capabilities.Add("activity", true, true, true) - return result -} - -func AssistantClaims(seconds int, workspace string) *Claims { - result := NewClaims("Assistant", fmt.Sprintf(WorkspaceApi, workspace), seconds) - result.Capabilities.Add("assistant", true, true, true) +func EditRobotClaims(seconds int, workspace string) *Claims { + result := NewClaims("EditRobot", fmt.Sprintf(WorkspaceApi, workspace), seconds) + result.CapabilitySet = "edit/robot" return result } -func RobotClaims(seconds int, workspace string) *Claims { - result := NewClaims("Robot", fmt.Sprintf(WorkspaceApi, workspace), seconds) - result.Capabilities.Add("package", true, true, true) +func RunAssistantClaims(seconds int, workspace string) *Claims { + result := NewClaims("RunAssistant", fmt.Sprintf(WorkspaceApi, workspace), seconds) + result.CapabilitySet = "run/assistant" return result } -func RunClaims(seconds int, workspace string) *Claims { - result := NewClaims("Run", fmt.Sprintf(WorkspaceApi, workspace), seconds) - result.Capabilities.Add("secret", true, true, true) - result.Capabilities.Add("artifact", false, false, true) - result.Capabilities.Add("livedata", false, true, true) - result.Capabilities.Add("workitemdata", false, true, true) +func RunRobotClaims(seconds int, workspace string) *Claims { + result := NewClaims("RunRobot", fmt.Sprintf(WorkspaceApi, workspace), seconds) + result.CapabilitySet = "run/robot" + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.capabilityset.runrobot", common.Version) return result } -func WorkspaceTreeClaims(seconds int) *Claims { - result := NewClaims("User", UserApi, seconds) - result.Capabilities.Add("workspace", true, false, false) - result.Capabilities.Add("workspaceTree", true, true, false) +func ViewWorkspacesClaims(seconds int) *Claims { + result := NewClaims("ViewWorkspaces", UserApi, seconds) + result.CapabilitySet = "view/workspaces" return result } diff --git a/operations/authorize_test.go b/operations/authorize_test.go index 4ec6465c..646440d5 100644 --- a/operations/authorize_test.go +++ b/operations/authorize_test.go @@ -70,35 +70,23 @@ func TestCanCreateNewClaims(t *testing.T) { sut := operations.NewClaims("Mega", "https://some.com", 232) wont_be.Nil(sut) - must_be.Equal(len(sut.Capabilities), 0) - sut.Capabilities.Add("secret", true, true, false) - sut.Capabilities.Add("artifact", false, true, true) - sut.Capabilities.Add("livedata", false, true, true) - sut.Capabilities.Add("workitemdata", false, true, true) - sut.Capabilities.Add("workspace", true, false, false) - sut.Capabilities.Add("workspaceTree", true, true, false) - sut.Capabilities.Add("package", true, true, true) - must_be.Equal(len(sut.Capabilities), 7) + sut.CapabilitySet = "run/assistant" output, err := sut.AsJson() must_be.Nil(err) wont_be.Nil(output) - must_be.True(strings.Contains(output, "workspaceTree")) - must_be.True(strings.Contains(output, "true")) - must_be.True(strings.Contains(output, "false")) - must_be.True(strings.Contains(output, "list")) - must_be.True(strings.Contains(output, "read")) - must_be.True(strings.Contains(output, "write")) + must_be.True(strings.Contains(output, "capabilitySet")) + must_be.True(strings.Contains(output, "run/assistant")) } func TestCanCreateRobotClaims(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) setup := operations.NewClaims("Robot", "https://some.com", 60) - setup.Capabilities.Add("package", true, true, true) + setup.CapabilitySet = "edit/robot" expected, err := setup.AsJson() must_be.Nil(err) - sut := operations.RobotClaims(60, "99") + sut := operations.EditRobotClaims(60, "99") wont_be.Nil(sut) result, err := sut.AsJson() must_be.Nil(err) @@ -106,18 +94,15 @@ func TestCanCreateRobotClaims(t *testing.T) { must_be.True(strings.Contains(sut.Url, "/workspaces/99/")) } -func TestCanCreateRunClaims(t *testing.T) { +func TestCanCreateRunRobotClaims(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) setup := operations.NewClaims("Run", "https://some.com", 88) - setup.Capabilities.Add("secret", true, true, true) - setup.Capabilities.Add("artifact", false, false, true) - setup.Capabilities.Add("livedata", false, true, true) - setup.Capabilities.Add("workitemdata", false, true, true) + setup.CapabilitySet = "run/robot" expected, err := setup.AsJson() must_be.Nil(err) - sut := operations.RunClaims(88, "777") + sut := operations.RunRobotClaims(88, "777") wont_be.Nil(sut) result, err := sut.AsJson() must_be.Nil(err) @@ -146,16 +131,15 @@ func TestCanGetVerificationClaims(t *testing.T) { must_be.Equal("GET", sut.Method) } -func TestCanCreateWorkspaceTreeClaims(t *testing.T) { +func TestCanCreateViewWorkspacesClaims(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) setup := operations.NewClaims("User", "https://some.com", 49) - setup.Capabilities.Add("workspace", true, false, false) - setup.Capabilities.Add("workspaceTree", true, true, false) + setup.CapabilitySet = "view/workspaces" expected, err := setup.AsJson() must_be.Nil(err) - sut := operations.WorkspaceTreeClaims(49) + sut := operations.ViewWorkspacesClaims(49) wont_be.Nil(sut) result, err := sut.AsJson() must_be.Nil(err) @@ -171,7 +155,7 @@ func TestCanCallAuthorizeCommand(t *testing.T) { wont_be.Nil(account) first := cloud.Response{Status: 200, Body: []byte("{\"token\":\"foo\",\"expiresIn\":1}")} client := mocks.NewClient(&first) - claims := operations.RunClaims(1, "777") + claims := operations.RunRobotClaims(1, "777") token, err := operations.AuthorizeCommand(client, account, claims) must_be.Nil(err) wont_be.Nil(token) diff --git a/operations/running.go b/operations/running.go index e4827d4f..6a07704c 100644 --- a/operations/running.go +++ b/operations/running.go @@ -118,7 +118,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t } var data Token if len(flags.WorkspaceId) > 0 { - claims := RunClaims(flags.ValidityTime*60, flags.WorkspaceId) + claims := RunRobotClaims(flags.ValidityTime*60, flags.WorkspaceId) data, err = AuthorizeClaims(flags.AccountName, claims) } if err != nil { @@ -171,7 +171,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } var data Token if !flags.Assistant && len(flags.WorkspaceId) > 0 { - claims := RunClaims(flags.ValidityTime*60, flags.WorkspaceId) + claims := RunRobotClaims(flags.ValidityTime*60, flags.WorkspaceId) data, err = AuthorizeClaims(flags.AccountName, claims) } if err != nil { diff --git a/operations/updownload.go b/operations/updownload.go index c39b2147..068866fc 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -33,7 +33,7 @@ func fetchRobotToken(client cloud.Client, account *account, claims *Claims) (str } func summonAssistantToken(client cloud.Client, account *account, workspaceId string) (string, error) { - claims := AssistantClaims(30*60, workspaceId) + claims := RunAssistantClaims(30*60, workspaceId) token, ok := account.Cached(claims.Name, claims.Url) if ok { return token, nil @@ -42,7 +42,7 @@ func summonAssistantToken(client cloud.Client, account *account, workspaceId str } func summonRobotToken(client cloud.Client, account *account, workspaceId string) (string, error) { - claims := RobotClaims(30*60, workspaceId) + claims := EditRobotClaims(30*60, workspaceId) token, ok := account.Cached(claims.Name, claims.Url) if ok { return token, nil diff --git a/operations/workspaces.go b/operations/workspaces.go index f63be7f7..749654ac 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -39,7 +39,7 @@ func fetchAnyToken(client cloud.Client, account *account, claims *Claims) (strin } func summonActivityToken(client cloud.Client, account *account, workspace string) (string, error) { - claims := ActivityClaims(15*60, workspace) + claims := EditRobotClaims(15*60, workspace) token, ok := account.Cached(claims.Name, claims.Url) if ok { return token, nil @@ -48,7 +48,7 @@ func summonActivityToken(client cloud.Client, account *account, workspace string } func summonWorkspaceToken(client cloud.Client, account *account) (string, error) { - claims := WorkspaceTreeClaims(15 * 60) + claims := ViewWorkspacesClaims(15 * 60) token, ok := account.Cached(claims.Name, claims.Url) if ok { return token, nil From af434ec770c07720d10a3f4a3ea4d424a356a026 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 7 May 2021 13:07:24 +0300 Subject: [PATCH 128/516] RCC-173: use new authorization scheme (v9.11.1) - new get/robot capabilitySet added into rcc - added User-Agent to rcc web requests --- cloud/client.go | 1 + common/variables.go | 5 +++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/assistant.go | 1 + operations/authorize.go | 6 ++++++ operations/diagnostics.go | 1 + operations/updownload.go | 8 ++++---- operations/workspaces.go | 4 ++-- 9 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index af1f0e1e..f04194d9 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -96,6 +96,7 @@ func (it *internalClient) does(method string, request *Request) *Response { httpRequest.TransferEncoding = []string{request.TransferEncoding} } httpRequest.Header.Add("robocorp-installation-id", xviper.TrackingIdentity()) + httpRequest.Header.Add("User-Agent", common.UserAgent()) for name, value := range request.Headers { httpRequest.Header.Add(name, value) } diff --git a/common/variables.go b/common/variables.go index a38a6d36..1db5c225 100644 --- a/common/variables.go +++ b/common/variables.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "time" ) @@ -108,6 +109,10 @@ func ForceDebug() { UnifyVerbosityFlags() } +func UserAgent() string { + return fmt.Sprintf("rcc/%s (%s %s) %s", Version, runtime.GOOS, runtime.GOARCH, ControllerIdentity()) +} + func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } diff --git a/common/version.go b/common/version.go index 0b7d8aa9..fc65f4ab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.11.0` + Version = `v9.11.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1cb7ece5..2fa226f4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.11.1 (date: 7.5.2021) + +- new get/robot capabilitySet added into rcc +- added User-Agent to rcc web requests + ## v9.11.0 (date: 6.5.2021) - started using new capabilitySet feature of cloud authorization diff --git a/operations/assistant.go b/operations/assistant.go index c69b4145..387782db 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -189,6 +189,7 @@ func MultipartUpload(url string, fields map[string]string, basename, fullpath st return err } request.Header.Add("Content-Type", many.FormDataContentType()) + request.Header.Add("User-Agent", common.UserAgent()) client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} response, err := client.Do(request) if err != nil { diff --git a/operations/authorize.go b/operations/authorize.go index a994084f..05465f6b 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -111,6 +111,12 @@ func RunRobotClaims(seconds int, workspace string) *Claims { return result } +func GetRobotClaims(seconds int, workspace string) *Claims { + result := NewClaims("GetRobot", fmt.Sprintf(WorkspaceApi, workspace), seconds) + result.CapabilitySet = "get/robot" + return result +} + func ViewWorkspacesClaims(seconds int) *Claims { result := NewClaims("ViewWorkspaces", UserApi, seconds) result.CapabilitySet = "view/workspaces" diff --git a/operations/diagnostics.go b/operations/diagnostics.go index fff18fd0..ca88b932 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -57,6 +57,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["working-dir"] = justText(os.Getwd) result.Details["tempdir"] = os.TempDir() result.Details["controller"] = common.ControllerIdentity() + result.Details["user-agent"] = common.UserAgent() result.Details["installationId"] = xviper.TrackingIdentity() result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) diff --git a/operations/updownload.go b/operations/updownload.go index 068866fc..c4e092b3 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -41,8 +41,8 @@ func summonAssistantToken(client cloud.Client, account *account, workspaceId str return fetchRobotToken(client, account, claims) } -func summonRobotToken(client cloud.Client, account *account, workspaceId string) (string, error) { - claims := EditRobotClaims(30*60, workspaceId) +func summonGetRobotToken(client cloud.Client, account *account, workspaceId string) (string, error) { + claims := GetRobotClaims(30*60, workspaceId) token, ok := account.Cached(claims.Name, claims.Url) if ok { return token, nil @@ -110,7 +110,7 @@ func getContent(client cloud.Client, awsUrl, zipfile string) error { } func UploadCommand(client cloud.Client, account *account, workspaceId, robotId, zipfile string, debug bool) error { - token, err := summonRobotToken(client, account, workspaceId) + token, err := summonEditRobotToken(client, account, workspaceId) if err != nil { return err } @@ -136,7 +136,7 @@ func UploadCommand(client cloud.Client, account *account, workspaceId, robotId, func DownloadCommand(client cloud.Client, account *account, workspaceId, robotId, zipfile string, debug bool) error { common.Timeline("download started: %s", zipfile) - token, err := summonRobotToken(client, account, workspaceId) + token, err := summonGetRobotToken(client, account, workspaceId) if err != nil { return err } diff --git a/operations/workspaces.go b/operations/workspaces.go index 749654ac..5efb45c2 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -38,7 +38,7 @@ func fetchAnyToken(client cloud.Client, account *account, claims *Claims) (strin return "", errors.New("Could not get authorization token.") } -func summonActivityToken(client cloud.Client, account *account, workspace string) (string, error) { +func summonEditRobotToken(client cloud.Client, account *account, workspace string) (string, error) { claims := EditRobotClaims(15*60, workspace) token, ok := account.Cached(claims.Name, claims.Url) if ok { @@ -132,7 +132,7 @@ func RobotDigestCommand(client cloud.Client, account *account, workspaceId, robo } func NewRobotCommand(client cloud.Client, account *account, workspace, robotName string) (Token, error) { - credentials, err := summonActivityToken(client, account, workspace) + credentials, err := summonEditRobotToken(client, account, workspace) if err != nil { return nil, err } From 54dcf35e929b03c5096f2b351e91a989205c1cd0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 11 May 2021 16:04:41 +0300 Subject: [PATCH 129/516] FIXES: holotree windows performance improvements (v9.11.2) - added query cache in front of slow "has blueprint" query (windows) - more timeline entries added for timing purposes --- common/version.go | 2 +- conda/robocorp.go | 3 ++- docs/changelog.md | 5 +++++ htfs/commands.go | 3 +++ htfs/directory.go | 3 +++ htfs/library.go | 26 ++++++++++++++++++++------ operations/running.go | 1 + 7 files changed, 35 insertions(+), 8 deletions(-) diff --git a/common/version.go b/common/version.go index fc65f4ab..5f655952 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.11.1` + Version = `v9.11.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 4bc4c301..2df1b548 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -220,7 +220,7 @@ func asVersion(text string) (uint64, string) { } func MicromambaVersion() string { - versionText, _, err := shell.New(CondaEnvironment(), ".", BinMicromamba(), "--version").CaptureOutput() + versionText, _, err := shell.New(CondaEnvironment(), ".", BinMicromamba(), "--repodata-ttl", "90000", "--version").CaptureOutput() if err != nil { return err.Error() } @@ -235,6 +235,7 @@ func HasMicroMamba() bool { version, versionText := asVersion(MicromambaVersion()) goodEnough := version >= 9002 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) + common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough } diff --git a/docs/changelog.md b/docs/changelog.md index 2fa226f4..ad22c3e6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.11.2 (date: 11.5.2021) + +- added query cache in front of slow "has blueprint" query (windows) +- more timeline entries added for timing purposes + ## v9.11.1 (date: 7.5.2021) - new get/robot capabilitySet added into rcc diff --git a/htfs/commands.go b/htfs/commands.go index 0efefc06..8437e4da 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -16,6 +16,7 @@ import ( func NewEnvironment(force bool, condafile string) (label string, err error) { defer fail.Around(&err) + common.Timeline("new holotree environment") _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) @@ -23,9 +24,11 @@ func NewEnvironment(force bool, condafile string) (label string, err error) { tree, err := New() fail.On(err != nil, "%s", err) + common.Timeline("holotree library created") if !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { tree = Virtual() + common.Timeline("downgraded to virtual holotree library") } err = RecordEnvironment(tree, holotreeBlueprint, force) fail.On(err != nil, "%s", err) diff --git a/htfs/directory.go b/htfs/directory.go index f4ebe7e3..60d6292a 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -134,6 +134,8 @@ func (it *Root) SaveAs(filename string) error { } func (it *Root) LoadFrom(filename string) error { + common.Timeline("holotree load %q", filename) + defer common.Timeline("holotree load done") source, err := os.Open(filename) if err != nil { return err @@ -146,6 +148,7 @@ func (it *Root) LoadFrom(filename string) error { defer reader.Close() content := bytes.NewBuffer(nil) io.Copy(content, reader) + common.Timeline("holotree load unmarshal") return json.Unmarshal(content.Bytes(), &it) } diff --git a/htfs/library.go b/htfs/library.go index 98cb9826..e7cfc0ac 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -49,8 +49,9 @@ type Library interface { } type hololib struct { - identity uint64 - basedir string + identity uint64 + basedir string + queryCache map[string]bool } func (it *hololib) Location(digest string) string { @@ -109,15 +110,27 @@ func (it *hololib) CatalogPath(key string) string { } func (it *hololib) HasBlueprint(blueprint []byte) bool { - catalog := it.CatalogPath(BlueprintHash(blueprint)) + key := BlueprintHash(blueprint) + found, ok := it.queryCache[key] + if !ok { + found = it.queryBlueprint(key) + it.queryCache[key] = found + } + return found +} + +func (it *hololib) queryBlueprint(key string) bool { + common.Timeline("holotree blueprint query") + catalog := it.CatalogPath(key) if !pathlib.IsFile(catalog) { return false } - tempdir := filepath.Join(conda.RobocorpTemp(), BlueprintHash(blueprint)) + tempdir := filepath.Join(conda.RobocorpTemp(), key) shadow, err := NewRoot(tempdir) if err != nil { return false } + common.Timeline("holotree load catalog") err = shadow.LoadFrom(catalog) if err != nil { common.Debug("Catalog load failed, reason: %v", err) @@ -249,8 +262,9 @@ func New() (Library, error) { } basedir := common.RobocorpHome() return &hololib{ - identity: sipit([]byte(basedir)), - basedir: basedir, + identity: sipit([]byte(basedir)), + basedir: basedir, + queryCache: make(map[string]bool), }, nil } diff --git a/operations/running.go b/operations/running.go index 6a07704c..67ecc205 100644 --- a/operations/running.go +++ b/operations/running.go @@ -60,6 +60,7 @@ func LoadAnyTaskEnvironment(packfile string, force bool) (bool, robot.Robot, rob } func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot.Robot, robot.Task, string) { + common.Timeline("task environment load started") FixRobot(packfile) config, err := robot.LoadRobotYaml(packfile, true) if err != nil { From 3cc4ee0bca486bb73c49d50a31b60cd10d72f0a0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 12 May 2021 12:47:45 +0300 Subject: [PATCH 130/516] RCC-174: holotree error signaling improvements (v9.11.3) - adding error signaling on anywork background workers - more work on improving slow parts of holotree - fixed settings.yaml conda link (conda.anaconda.org reference) --- anywork/worker.go | 33 +++++++++++++++++++++++++++++---- assets/settings.yaml | 4 ++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/directory.go | 22 ++++++++++------------ htfs/library.go | 21 ++++++++++++++++----- 6 files changed, 64 insertions(+), 24 deletions(-) diff --git a/anywork/worker.go b/anywork/worker.go index 8c8d186a..e23dad3c 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -9,16 +9,20 @@ import ( var ( group *sync.WaitGroup pipeline WorkQueue + failpipe Failures + errcount Counters headcount uint64 ) type Work func() type WorkQueue chan Work +type Failures chan string +type Counters chan uint64 func catcher(title string, identity uint64) { catch := recover() if catch != nil { - fmt.Fprintf(os.Stderr, "Recovering %q #%d: %v\n", title, identity, catch) + failpipe <- fmt.Sprintf("Recovering %q #%d: %v", title, identity, catch) } } @@ -39,11 +43,27 @@ func member(identity uint64) { } } +func watcher(failures Failures, counters Counters) { + counter := uint64(0) + for { + select { + case fail := <-failures: + counter += 1 + fmt.Fprintln(os.Stderr, fail) + case counters <- counter: + counter = 0 + } + } +} + func init() { group = &sync.WaitGroup{} pipeline = make(WorkQueue, 100000) + failpipe = make(Failures) + errcount = make(Counters) headcount = 1 go member(headcount) + go watcher(failpipe, errcount) } func Scale(limit uint64) { @@ -60,11 +80,16 @@ func Backlog(todo Work) { } } -func Sync() { +func Sync() error { group.Wait() + count := <-errcount + if count > 0 { + return fmt.Errorf("There has been %d failures. See messages above.", count) + } + return nil } -func Done() { +func Done() error { close(pipeline) - group.Wait() + return Sync() } diff --git a/assets/settings.yaml b/assets/settings.yaml index 413a5bc7..cbb93daa 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -4,7 +4,7 @@ endpoints: cloud-ui: https://cloud.robocorp.com/ pypi: # https://pypi.org/simple/ pypi-trusted: # https://pypi.org/ - conda: # https://repo.anaconda.org/ + conda: # https://conda.anaconda.org/ downloads: https://downloads.robocorp.com/ docs: https://robocorp.com/docs/ telemetry: https://telemetry.robocorp.com/ @@ -13,7 +13,7 @@ endpoints: diagnostics-hosts: - files.pythonhosted.org - github.com - - repo.anaconda.org + - conda.anaconda.org - pypi.org autoupdates: diff --git a/common/version.go b/common/version.go index 5f655952..fa0b8e6a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.11.2` + Version = `v9.11.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index ad22c3e6..8f6be484 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.11.3 (date: 12.5.2021) + +- adding error signaling on anywork background workers +- more work on improving slow parts of holotree +- fixed settings.yaml conda link (conda.anaconda.org reference) + ## v9.11.2 (date: 11.5.2021) - added query cache in front of slow "has blueprint" query (windows) diff --git a/htfs/directory.go b/htfs/directory.go index 60d6292a..17993eb1 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -1,11 +1,9 @@ package htfs import ( - "bytes" "compress/gzip" "encoding/json" "fmt" - "io" "io/fs" "os" "path/filepath" @@ -89,21 +87,23 @@ func (it *Root) Lift() error { func (it *Root) Treetop(task Treetop) error { err := task(it.Path, it.Tree) + if err != nil { + return err + } common.Timeline("holotree treetop sync") - anywork.Sync() - return err + return anywork.Sync() } -func (it *Root) AllDirs(task Dirtask) { +func (it *Root) AllDirs(task Dirtask) error { it.Tree.AllDirs(it.Path, task) common.Timeline("holotree dirs sync") - anywork.Sync() + return anywork.Sync() } -func (it *Root) AllFiles(task Filetask) { +func (it *Root) AllFiles(task Filetask) error { it.Tree.AllFiles(it.Path, task) common.Timeline("holotree files sync") - anywork.Sync() + return anywork.Sync() } func (it *Root) AsJson() ([]byte, error) { @@ -146,10 +146,8 @@ func (it *Root) LoadFrom(filename string) error { return err } defer reader.Close() - content := bytes.NewBuffer(nil) - io.Copy(content, reader) - common.Timeline("holotree load unmarshal") - return json.Unmarshal(content.Bytes(), &it) + decoder := json.NewDecoder(reader) + return decoder.Decode(&it) } type Dir struct { diff --git a/htfs/library.go b/htfs/library.go index e7cfc0ac..9bf9b7fb 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -88,7 +88,10 @@ func (it *hololib) Record(blueprint []byte) error { return err } common.Timeline("holotree (re)locator start") - fs.AllFiles(Locator(it.Identity())) + err = fs.AllFiles(Locator(it.Identity())) + if err != nil { + return err + } common.Timeline("holotree (re)locator done") fs.Blueprint = key catalog := filepath.Join(common.HololibLocation(), "catalog", key) @@ -130,7 +133,6 @@ func (it *hololib) queryBlueprint(key string) bool { if err != nil { return false } - common.Timeline("holotree load catalog") err = shadow.LoadFrom(catalog) if err != nil { common.Debug("Catalog load failed, reason: %v", err) @@ -212,7 +214,10 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { } score := &stats{} common.Timeline("holotree restore start") - fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + if err != nil { + return "", err + } common.Timeline("holotree restore done") defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) @@ -307,7 +312,10 @@ func (it *virtual) Record(blueprint []byte) error { return err } common.Timeline("holotree (re)locator start (virtual)") - fs.AllFiles(Locator(it.Identity())) + err = fs.AllFiles(Locator(it.Identity())) + if err != nil { + return err + } common.Timeline("holotree (re)locator done (virtual)") it.registry = make(map[string]string) fs.Treetop(DigestMapper(it.registry)) @@ -349,7 +357,10 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { } score := &stats{} common.Timeline("holotree restore start (virtual)") - fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + if err != nil { + return "", err + } common.Timeline("holotree restore done (virtual)") defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) From a411a9daf593c8312994f7539d076050d58f42b6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 18 May 2021 14:33:41 +0300 Subject: [PATCH 131/516] RCC-176: more verbose micromamba and pip (v9.12.0) - new environment variable `RCC_VERBOSE_ENVIRONMENT_BUILDING` to make environment building more verbose - with above variable and `--trace` or `--debug` flags, both micromamba and pip are run with more verbosity --- common/commander.go | 7 +++++++ common/variables.go | 8 +++++++- common/version.go | 2 +- conda/workflows.go | 2 ++ docs/changelog.md | 7 +++++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/commander.go b/common/commander.go index 5f5d77b4..1a37545c 100644 --- a/common/commander.go +++ b/common/commander.go @@ -14,6 +14,13 @@ func (it *Commander) Option(name, value string) *Commander { return it } +func (it *Commander) ConditionalFlag(condition bool, name string) *Commander { + if condition { + it.command = append(it.command, name) + } + return it +} + func (it *Commander) CLI() []string { return it.command } diff --git a/common/variables.go b/common/variables.go index 1db5c225..5f59efe0 100644 --- a/common/variables.go +++ b/common/variables.go @@ -10,7 +10,8 @@ import ( ) const ( - ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ) var ( @@ -45,6 +46,11 @@ func RobocorpHome() string { return ensureDirectory(ExpandPath(defaultRobocorpLocation)) } +func VerboseEnvironmentBuilding() bool { + verbose := len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 + return verbose || DebugFlag || TraceFlag +} + func ensureDirectory(name string) string { os.MkdirAll(name, 0o750) return name diff --git a/common/version.go b/common/version.go index fa0b8e6a..7763fb09 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.11.3` + Version = `v9.12.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index dbe9ea4d..bd568850 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -193,6 +193,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) + mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") common.Timeline("Micromamba start.") @@ -221,6 +222,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) + pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== new live --- pip install phase ===") err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index 8f6be484..f2734598 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v9.12.0 (date: 18.5.2021) + +- new environment variable `RCC_VERBOSE_ENVIRONMENT_BUILDING` to make + environment building more verbose +- with above variable and `--trace` or `--debug` flags, both micromamba + and pip are run with more verbosity + ## v9.11.3 (date: 12.5.2021) - adding error signaling on anywork background workers From d3b594c29fe9da8a83724450488fc13faeb7a605 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 18 May 2021 15:52:46 +0300 Subject: [PATCH 132/516] RCC-177: override system requirements (v9.12.1) - new environment variable `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` to make skip those system requirements that some users are willing to try - first such thing is "long path support" on some versions of Windows --- cmd/longpaths.go | 4 ++++ common/variables.go | 9 +++++++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 8 ++++++-- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cmd/longpaths.go b/cmd/longpaths.go index e9efdd30..70c24ea7 100644 --- a/cmd/longpaths.go +++ b/cmd/longpaths.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -15,6 +16,9 @@ var longpathsCmd = &cobra.Command{ Short: "Check and enable Windows longpath support", Long: "Check and enable Windows longpath support", Run: func(cmd *cobra.Command, args []string) { + if common.OverrideSystemRequirements() { + pretty.Exit(100, "This operation is prevented, because ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS is effective!") + } var err error if enableLongpaths { err = conda.EnforceLongpathSupport() diff --git a/common/variables.go b/common/variables.go index 5f59efe0..6fc458e1 100644 --- a/common/variables.go +++ b/common/variables.go @@ -10,8 +10,9 @@ import ( ) const ( - ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` - VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` + ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` + ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` ) var ( @@ -51,6 +52,10 @@ func VerboseEnvironmentBuilding() bool { return verbose || DebugFlag || TraceFlag } +func OverrideSystemRequirements() bool { + return len(os.Getenv(ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS)) > 0 +} + func ensureDirectory(name string) string { os.MkdirAll(name, 0o750) return name diff --git a/common/version.go b/common/version.go index 7763fb09..aa9fdbed 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.12.0` + Version = `v9.12.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index f2734598..10450a57 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.12.1 (date: 18.5.2021) + +- new environment variable `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` to make + skip those system requirements that some users are willing to try +- first such thing is "long path support" on some versions of Windows + ## v9.12.0 (date: 18.5.2021) - new environment variable `RCC_VERBOSE_ENVIRONMENT_BUILDING` to make diff --git a/operations/diagnostics.go b/operations/diagnostics.go index ca88b932..1d89dfa8 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -51,6 +51,8 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["stats"] = rccStatusLine() result.Details["micromamba"] = conda.MicromambaVersion() result.Details["ROBOCORP_HOME"] = common.RobocorpHome() + result.Details["ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS"] = fmt.Sprintf("%v", common.OverrideSystemRequirements()) + result.Details["RCC_VERBOSE_ENVIRONMENT_BUILDING"] = fmt.Sprintf("%v", common.VerboseEnvironmentBuilding()) result.Details["user-cache-dir"] = justText(os.UserCacheDir) result.Details["user-config-dir"] = justText(os.UserConfigDir) result.Details["user-home-dir"] = justText(os.UserHomeDir) @@ -70,7 +72,9 @@ func RunDiagnostics() *common.DiagnosticStatus { // checks result.Checks = append(result.Checks, robocorpHomeCheck()) - result.Checks = append(result.Checks, longPathSupportCheck()) + if !common.OverrideSystemRequirements() { + result.Checks = append(result.Checks, longPathSupportCheck()) + } for _, host := range settings.Global.Hostnames() { result.Checks = append(result.Checks, dnsLookupCheck(host)) } @@ -190,7 +194,7 @@ func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { sort.Strings(keys) for _, key := range keys { value := details.Details[key] - fmt.Fprintf(sink, " - %-25s... %q\n", key, value) + fmt.Fprintf(sink, " - %-38s... %q\n", key, value) } fmt.Fprintln(sink, "") fmt.Fprintln(sink, "Checks:") From 3bbd507ece03651136dac118629a01e212f8f95f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 18 May 2021 16:43:27 +0300 Subject: [PATCH 133/516] RCC-170: micromamba upgrades and fixes (v9.13.0) - micromamba upgrade to version 0.13.1 - activation script fix for windows environment --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 4 ++-- conda/robocorp.go | 2 +- docs/changelog.md | 5 +++++ 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index aa9fdbed..38fbb5aa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.12.1` + Version = `v9.13.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index ddfe1b0e..317d6785 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.9.2/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.13.1/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 4d33cf85..e90d33b7 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.9.2/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.13.1/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 694777e4..bd8a3684 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -21,14 +21,14 @@ const ( usrSuffix = "\\usr" binSuffix = "\\bin" activateScript = "@echo off\n" + - "set MAMBA_ROOT_PREFIX=\"{{.Robocorphome}}\"\n" + + "set \"MAMBA_ROOT_PREFIX={{.Robocorphome}}\"\n" + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell -s cmd.exe activate -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + "call \"{{.Rcc}}\" internal env -l after\n" commandSuffix = ".cmd" ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.9.2/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.13.1/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 2df1b548..9a40c97e 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -233,7 +233,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 9002 + goodEnough := version >= 13001 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index 10450a57..21faca75 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.13.0 (date: 18.5.2021) + +- micromamba upgrade to version 0.13.1 +- activation script fix for windows environment + ## v9.12.1 (date: 18.5.2021) - new environment variable `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` to make From 3d10bb32ff585f0127905b755537ddef66947701 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 May 2021 11:33:57 +0300 Subject: [PATCH 134/516] RCC-175: diagnostics improvements (v9.14.0) - added PYTHONPATH diagnostics validation - added `--production` flag to diagnostics commands --- cmd/diagnostics.go | 3 ++- cmd/robotdiagnostics.go | 3 ++- cmd/sharedvariables.go | 13 +++++++------ common/version.go | 2 +- conda/condayaml.go | 36 +++++++++++++++++++++++++++++++++--- docs/changelog.md | 5 +++++ operations/diagnostics.go | 36 ++++++++++++++++++++++++++++-------- operations/issues.go | 2 +- robot/robot.go | 6 +++--- 9 files changed, 82 insertions(+), 24 deletions(-) diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index bbe19cdf..01e0bee6 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -21,7 +21,7 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag) + _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -34,4 +34,5 @@ func init() { diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") diagnosticsCmd.Flags().StringVarP(&fileOption, "file", "f", "", "Save output into a file.") diagnosticsCmd.Flags().StringVarP(&robotOption, "robot", "r", "", "Full path to 'robot.yaml' configuration file. [optional]") + diagnosticsCmd.Flags().BoolVarP(&productionFlag, "production", "p", false, "Checks for production level robots. [optional]") } diff --git a/cmd/robotdiagnostics.go b/cmd/robotdiagnostics.go index 747daa09..7054d53c 100644 --- a/cmd/robotdiagnostics.go +++ b/cmd/robotdiagnostics.go @@ -16,7 +16,7 @@ var robotDiagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - err := operations.PrintRobotDiagnostics(robotFile, jsonFlag) + err := operations.PrintRobotDiagnostics(robotFile, jsonFlag, productionFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -27,5 +27,6 @@ var robotDiagnosticsCmd = &cobra.Command{ func init() { robotCmd.AddCommand(robotDiagnosticsCmd) robotDiagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + robotDiagnosticsCmd.Flags().BoolVarP(&productionFlag, "production", "p", false, "Checks for production level robots.") robotDiagnosticsCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") } diff --git a/cmd/sharedvariables.go b/cmd/sharedvariables.go index 7699fbc9..e56f3fe8 100644 --- a/cmd/sharedvariables.go +++ b/cmd/sharedvariables.go @@ -2,12 +2,13 @@ package cmd // flags var ( - autoInstall bool - defaultFlag bool - forceFlag bool - listFlag bool - jsonFlag bool - verifiedFlag bool + autoInstall bool + defaultFlag bool + forceFlag bool + listFlag bool + jsonFlag bool + productionFlag bool + verifiedFlag bool ) // options diff --git a/common/version.go b/common/version.go index 38fbb5aa..75d6733a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.13.0` + Version = `v9.14.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 95920215..2b1bba9e 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -54,6 +54,11 @@ func AsDependency(value string) *Dependency { } } +func (it *Dependency) Representation() string { + parts := strings.SplitN(strings.ToLower(it.Name), "[", 2) + return parts[0] +} + func (it *Dependency) IsExact() bool { return len(it.Qualifier)+len(it.Versions) > 0 } @@ -373,8 +378,13 @@ func (it *Environment) AsRequirementsText() string { return strings.Join(lines, Newline) } -func (it *Environment) Diagnostics(target *common.DiagnosticStatus) { +func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose := target.Diagnose("Conda") + notice := diagnose.Warning + if production { + notice = diagnose.Fail + } + packages := make(map[string]bool) countChannels := len(it.Channels) defaultsPostion := -1 floating := false @@ -390,13 +400,23 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus) { diagnose.Warning("", "Try to avoid putting defaults channel as first channel.") ok = false } + if countChannels > 1 { + diagnose.Warning("", "Try to avoid multiple channel. They may cause problems with code compatibility.") + ok = false + } if ok { diagnose.Ok("Channels in conda.yaml are ok.") } ok = true + condaCount := len(it.Conda) for _, dependency := range it.Conda { + presentation := dependency.Representation() + if packages[presentation] { + notice("", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) + } + packages[presentation] = true if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { - diagnose.Warning("", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + notice("", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false floating = true } @@ -416,8 +436,13 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus) { ok = false } for _, dependency := range it.Pip { + presentation := dependency.Representation() + if packages[presentation] { + notice("", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) + } + packages[presentation] = true if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { - diagnose.Warning("", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + notice("", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false floating = true } @@ -430,6 +455,11 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus) { if ok { diagnose.Ok("Pip dependencies in conda.yaml are ok.") } + totalCount := condaCount + pipCount + if totalCount > 10 { + diagnose.Warning("", "There are more than 10 dependencies in conda.yaml [conda: %d, pip: %d]. This might cause problems for dependency resolvers.", condaCount, pipCount) + diagnose.Warning("", "Too many dependencies might also indicate lack of focus, doing too much, doing it wrong, or missing cleanup step in development process.") + } if floating { diagnose.Warning("", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") } diff --git a/docs/changelog.md b/docs/changelog.md index 21faca75..9408d31f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.14.0 (date: 19.5.2021) + +- added PYTHONPATH diagnostics validation +- added `--production` flag to diagnostics commands + ## v9.13.0 (date: 18.5.2021) - micromamba upgrade to version 0.13.1 diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 1d89dfa8..304d55ab 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -72,6 +72,7 @@ func RunDiagnostics() *common.DiagnosticStatus { // checks result.Checks = append(result.Checks, robocorpHomeCheck()) + result.Checks = append(result.Checks, pythonPathCheck()) if !common.OverrideSystemRequirements() { result.Checks = append(result.Checks, longPathSupportCheck()) } @@ -111,6 +112,25 @@ func longPathSupportCheck() *common.DiagnosticCheck { } } +func pythonPathCheck() *common.DiagnosticCheck { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + pythonPath := os.Getenv("PYTHONPATH") + if len(pythonPath) > 0 { + return &common.DiagnosticCheck{ + Type: "OS", + Status: statusWarning, + Message: fmt.Sprintf("PYTHONPATH is set to %q. This may cause problems.", pythonPath), + Link: supportGeneralUrl, + } + } + return &common.DiagnosticCheck{ + Type: "OS", + Status: statusOk, + Message: "PYTHONPATH is not set, which is good.", + Link: supportGeneralUrl, + } +} + func robocorpHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { @@ -214,7 +234,7 @@ func fileIt(filename string) (io.WriteCloser, error) { return file, nil } -func ProduceDiagnostics(filename, robotfile string, json bool) (*common.DiagnosticStatus, error) { +func ProduceDiagnostics(filename, robotfile string, json, production bool) (*common.DiagnosticStatus, error) { file, err := fileIt(filename) if err != nil { return nil, err @@ -222,7 +242,7 @@ func ProduceDiagnostics(filename, robotfile string, json bool) (*common.Diagnost defer file.Close() result := RunDiagnostics() if len(robotfile) > 0 { - addRobotDiagnostics(robotfile, result) + addRobotDiagnostics(robotfile, result, production) } settings.Global.Diagnostics(result) if json { @@ -270,29 +290,29 @@ func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { diagnoseFilesUnmarshal(yaml.Unmarshal, "YAML", rootdir, yamls, target) } -func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus) { +func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus, production bool) { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") config, err := robot.LoadRobotYaml(robotfile, false) diagnose := target.Diagnose("Robot") if err != nil { diagnose.Fail(supportGeneralUrl, "About robot.yaml: %v", err) } else { - config.Diagnostics(target) + config.Diagnostics(target, production) } addFileDiagnostics(filepath.Dir(robotfile), target) } -func RunRobotDiagnostics(robotfile string) *common.DiagnosticStatus { +func RunRobotDiagnostics(robotfile string, production bool) *common.DiagnosticStatus { result := &common.DiagnosticStatus{ Details: make(map[string]string), Checks: []*common.DiagnosticCheck{}, } - addRobotDiagnostics(robotfile, result) + addRobotDiagnostics(robotfile, result, production) return result } -func PrintRobotDiagnostics(robotfile string, json bool) error { - result := RunRobotDiagnostics(robotfile) +func PrintRobotDiagnostics(robotfile string, json, production bool) error { + result := RunRobotDiagnostics(robotfile, production) if json { jsonDiagnostics(os.Stdout, result) } else { diff --git a/operations/issues.go b/operations/issues.go index 1b3a8277..666cb6f8 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -60,7 +60,7 @@ func createIssueZip(attachmentsFiles []string) (string, error) { func createDiagnosticsReport(robotfile string) (string, *common.DiagnosticStatus, error) { file := filepath.Join(conda.RobocorpTemp(), "diagnostics.txt") - diagnostics, err := ProduceDiagnostics(file, robotfile, false) + diagnostics, err := ProduceDiagnostics(file, robotfile, false, false) if err != nil { return "", nil, err } diff --git a/robot/robot.go b/robot/robot.go index d52ffd33..a3aed8d8 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -28,7 +28,7 @@ type Robot interface { CondaHash() string RootDirectory() string Validate() (bool, error) - Diagnostics(*common.DiagnosticStatus) + Diagnostics(*common.DiagnosticStatus, bool) WorkingDirectory() string ArtifactDirectory() string @@ -129,7 +129,7 @@ func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { } } -func (it *robot) Diagnostics(target *common.DiagnosticStatus) { +func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose := target.Diagnose("Robot") it.diagnoseTasks(diagnose) it.diagnoseVariousPaths(diagnose) @@ -153,7 +153,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus) { if err != nil { diagnose.Fail("", "From robot.yaml, loading conda.yaml failed with: %v", err) } else { - condaEnv.Diagnostics(target) + condaEnv.Diagnostics(target, production) } } } From b05a98dfe1373b6a741b725432a055906862fdfc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 20 May 2021 16:41:03 +0300 Subject: [PATCH 135/516] RCC-178: experimental run arguments (v9.15.0) - for `task run` and `task testrun` there is now possibility to give additional arguments from commandline, by using `--` separator between normal rcc arguments and those intended for executed robot - rcc now considers "http://127.0.0.1" as special case that does not require https --- cloud/client.go | 12 ++++++++++-- cloud/client_test.go | 17 +++++++++++++++++ cmd/run.go | 5 +++-- cmd/testrun.go | 4 +++- common/version.go | 2 +- docs/changelog.md | 8 ++++++++ 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index f04194d9..82ce159c 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "path/filepath" "strings" @@ -49,10 +50,17 @@ type Client interface { func EnsureHttps(endpoint string) (string, error) { nice := strings.TrimRight(strings.TrimSpace(endpoint), "/") - if strings.HasPrefix(nice, "https://") { + parsed, err := url.Parse(nice) + if err != nil { + return "", err + } + if parsed.Host == "127.0.0.1" || strings.HasPrefix(parsed.Host, "127.0.0.1:") { return nice, nil } - return "", fmt.Errorf("Endpoint '%s' must start with https:// prefix.", nice) + if parsed.Scheme != "https" { + return "", fmt.Errorf("Endpoint '%s' must start with https:// prefix.", nice) + } + return nice, nil } func NewClient(endpoint string) (Client, error) { diff --git a/cloud/client_test.go b/cloud/client_test.go index 4929b57a..c979b898 100644 --- a/cloud/client_test.go +++ b/cloud/client_test.go @@ -29,3 +29,20 @@ func TestCanCreateClient(t *testing.T) { wont_be.Nil(sut) must_be.Nil(err) } + +func TestCanEnsureHttps(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + _, err := cloud.EnsureHttps("http://some.server.com/endpoint") + wont_be.Nil(err) + + incoming := "https://some.server.com/endpoint" + output, err := cloud.EnsureHttps(incoming) + must_be.Nil(err) + must_be.Equal(incoming, output) + + special := "http://127.0.0.1:8192/endpoint" + output, err = cloud.EnsureHttps(special) + must_be.Nil(err) + must_be.Equal(special, output) +} diff --git a/cmd/run.go b/cmd/run.go index c56f0890..b63dfed1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -23,7 +23,6 @@ var runCmd = &cobra.Command{ Short: "Run task in place, to debug current setup.", Long: `Local task run, in place, to see how full run execution works in your own machine.`, - Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() @@ -35,7 +34,9 @@ in your own machine.`, defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) - operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, interactiveFlag, nil) + commandline := todo.Commandline() + commandline = append(commandline, args...) + operations.SelectExecutionModel(captureRunFlags(false), simple, commandline, config, todo, label, interactiveFlag, nil) }, } diff --git a/cmd/testrun.go b/cmd/testrun.go index bc0c8e19..ce10b2e4 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -59,7 +59,9 @@ var testrunCmd = &cobra.Command{ simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) - operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, false, nil) + commandline := todo.Commandline() + commandline = append(commandline, args...) + operations.SelectExecutionModel(captureRunFlags(false), simple, commandline, config, todo, label, false, nil) }, } diff --git a/common/version.go b/common/version.go index 75d6733a..ba33cb30 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.14.0` + Version = `v9.15.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9408d31f..48cea271 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v9.15.0 (date: 20.5.2021) + +- for `task run` and `task testrun` there is now possibility to give additional + arguments from commandline, by using `--` separator between normal rcc + arguments and those intended for executed robot +- rcc now considers "http://127.0.0.1" as special case that does not require + https + ## v9.14.0 (date: 19.5.2021) - added PYTHONPATH diagnostics validation From 1625d0ab69b5bab13d3d9b37c9b3f7045cdbe867 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 21 May 2021 10:42:13 +0300 Subject: [PATCH 136/516] FIXES: small fixes and changes (v9.15.1) - added images as non-executable files - run and testrun commands have new option `--no-outputs` which prevent capture of stderr/stdout into files - separated `--trace` and `--debug` flags from `micromamba` and `pip` verbosity introduced in v9.12.0 (it is causing too much output and should be reserved only for `RCC_VERBOSE_ENVIRONMENT_BUILDING` variable --- cmd/run.go | 1 + cmd/testrun.go | 1 + common/variables.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 9 +++++++++ operations/fixing.go | 6 ++++++ operations/running.go | 18 +++++++++++++++--- shell/task.go | 8 ++++++++ 8 files changed, 43 insertions(+), 6 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index b63dfed1..7d09e7b4 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -64,4 +64,5 @@ func init() { runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") } diff --git a/cmd/testrun.go b/cmd/testrun.go index ce10b2e4..40885819 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -95,4 +95,5 @@ func init() { testrunCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") testrunCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") testrunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + testrunCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") } diff --git a/common/variables.go b/common/variables.go index 6fc458e1..897b6d69 100644 --- a/common/variables.go +++ b/common/variables.go @@ -21,6 +21,7 @@ var ( TraceFlag bool LogLinenumbers bool NoCache bool + NoOutputCapture bool Liveonly bool Stageonly bool LeaseEffective bool @@ -48,8 +49,7 @@ func RobocorpHome() string { } func VerboseEnvironmentBuilding() bool { - verbose := len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 - return verbose || DebugFlag || TraceFlag + return len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 } func OverrideSystemRequirements() bool { diff --git a/common/version.go b/common/version.go index ba33cb30..7280b5c6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.15.0` + Version = `v9.15.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 48cea271..1f48b69d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v9.15.1 (date: 21.5.2021) + +- added images as non-executable files +- run and testrun commands have new option `--no-outputs` which prevent + capture of stderr/stdout into files +- separated `--trace` and `--debug` flags from `micromamba` and `pip` verbosity + introduced in v9.12.0 (it is causing too much output and should be reserved + only for `RCC_VERBOSE_ENVIRONMENT_BUILDING` variable + ## v9.15.0 (date: 20.5.2021) - for `task run` and `task testrun` there is now possibility to give additional diff --git a/operations/fixing.go b/operations/fixing.go index d2ed05da..27c4ffe2 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -17,6 +17,12 @@ var ( ) func init() { + nonExecutableExtensions[".svg"] = true + nonExecutableExtensions[".bmp"] = true + nonExecutableExtensions[".png"] = true + nonExecutableExtensions[".gif"] = true + nonExecutableExtensions[".jpg"] = true + nonExecutableExtensions[".jpeg"] = true nonExecutableExtensions[".md"] = true nonExecutableExtensions[".txt"] = true nonExecutableExtensions[".htm"] = true diff --git a/operations/running.go b/operations/running.go index 67ecc205..dd008112 100644 --- a/operations/running.go +++ b/operations/running.go @@ -38,7 +38,11 @@ func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, enviro return false } common.Log("Installed pip packages:") - _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Tee(outputDir, false) + if common.NoOutputCapture { + _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Execute(false) + } else { + _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Tee(outputDir, false) + } if err != nil { return false } @@ -146,7 +150,11 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t } outputDir := config.ArtifactDirectory() common.Debug("DEBUG: about to run command - %v", task) - _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + if common.NoOutputCapture { + _, err = shell.New(environment, directory, task...).Execute(interactive) + } else { + _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + } if err != nil { pretty.Exit(9, "Error: %v", err) } @@ -204,7 +212,11 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro PipFreeze(searchPath, directory, outputDir, environment) } common.Debug("DEBUG: about to run command - %v", task) - _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + if common.NoOutputCapture { + _, err = shell.New(environment, directory, task...).Execute(interactive) + } else { + _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + } after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after, true) diff --git a/shell/task.go b/shell/task.go index 4fdd9eca..a975f413 100644 --- a/shell/task.go +++ b/shell/task.go @@ -73,6 +73,14 @@ func (it *Task) Transparent() (int, error) { return it.execute(os.Stdin, it.stdout(), os.Stderr) } +func (it *Task) Execute(interactive bool) (int, error) { + var stdin io.Reader = os.Stdin + if !interactive { + stdin = bytes.NewReader([]byte{}) + } + return it.execute(stdin, it.stdout(), os.Stderr) +} + func (it *Task) Tee(folder string, interactive bool) (int, error) { err := os.MkdirAll(folder, 0755) if err != nil { From 94546d47817347865ffeabbaf1f82bffae2b4f89 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 21 May 2021 11:51:37 +0300 Subject: [PATCH 137/516] RCC-179: catalog suffix based on system (v9.16.0) - catalog extension based on operating system, architecture and directory location --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/library.go | 12 ++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 7280b5c6..39a0b86e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.15.1` + Version = `v9.16.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1f48b69d..2e3acaab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.16.0 (date: 21.5.2021) + +- catalog extension based on operating system, architecture and directory + location + ## v9.15.1 (date: 21.5.2021) - added images as non-executable files diff --git a/htfs/library.go b/htfs/library.go index 9bf9b7fb..2d8175a0 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + "runtime" + "strings" "sync" "time" @@ -94,7 +96,7 @@ func (it *hololib) Record(blueprint []byte) error { } common.Timeline("holotree (re)locator done") fs.Blueprint = key - catalog := filepath.Join(common.HololibLocation(), "catalog", key) + catalog := it.CatalogPath(key) err = fs.SaveAs(catalog) if err != nil { return err @@ -109,7 +111,8 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - return filepath.Join(common.HololibLocation(), "catalog", key) + name := fmt.Sprintf("%s.%016x", key, it.identity) + return filepath.Join(common.HololibLocation(), "catalog", name) } func (it *hololib) HasBlueprint(blueprint []byte) bool { @@ -198,7 +201,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { if err != nil { return "", err } - err = fs.LoadFrom(filepath.Join(common.HololibLocation(), "catalog", key)) + err = fs.LoadFrom(it.CatalogPath(key)) if err != nil { return "", err } @@ -266,8 +269,9 @@ func New() (Library, error) { return nil, err } basedir := common.RobocorpHome() + identity := strings.ToLower(fmt.Sprintf("%s %s %q", runtime.GOOS, runtime.GOARCH, basedir)) return &hololib{ - identity: sipit([]byte(basedir)), + identity: sipit([]byte(identity)), basedir: basedir, queryCache: make(map[string]bool), }, nil From 4e5ad858b38cb6c683d2ea55c66278771e4c773a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 26 May 2021 11:26:20 +0300 Subject: [PATCH 138/516] RCC-180: exporting catalogs from holotree (v9.17.0) - added `export` command to holotree [experimental] --- cmd/holotreeExport.go | 69 +++++++++++++++++++++++++++++++++++++ common/variables.go | 8 +++++ common/version.go | 2 +- docs/changelog.md | 4 +++ htfs/functions.go | 27 +++++++++++++++ htfs/library.go | 80 +++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 cmd/holotreeExport.go diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go new file mode 100644 index 00000000..a2d02739 --- /dev/null +++ b/cmd/holotreeExport.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + holozip string +) + +func holotreeExport(catalogs []string, archive string) { + common.Debug("Exporting catalogs:") + for _, catalog := range catalogs { + common.Debug("- %s", catalog) + } + + tree, err := htfs.New() + pretty.Guard(err == nil, 2, "%s", err) + + err = tree.Export(catalogs, archive) + pretty.Guard(err == nil, 3, "%s", err) +} + +func listCatalogs() { + common.Log("Selectable catalogs (you can use substrings):") + for _, catalog := range htfs.Catalogs() { + common.Log("- %s", catalog) + } +} + +func selectCatalogs(filters []string) []string { + result := make([]string, 0, len(filters)) + for _, catalog := range htfs.Catalogs() { + for _, filter := range filters { + if strings.Contains(catalog, filter) { + result = append(result, catalog) + break + } + } + } + return result +} + +var holotreeExportCmd = &cobra.Command{ + Use: "export catalog+", + Short: "Export existing holotree catalog and library parts.", + Long: "Export existing holotree catalog and library parts.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree export command lasted").Report() + } + if len(args) == 0 { + listCatalogs() + } else { + holotreeExport(selectCatalogs(args), holozip) + } + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeExportCmd) + holotreeExportCmd.Flags().StringVarP(&holozip, "zipfile", "z", "hololib.zip", "Name of zipfile to export.") +} diff --git a/common/variables.go b/common/variables.go index 897b6d69..67b17938 100644 --- a/common/variables.go +++ b/common/variables.go @@ -81,6 +81,14 @@ func HololibLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "hololib")) } +func HololibCatalogLocation() string { + return ensureDirectory(filepath.Join(HololibLocation(), "catalog")) +} + +func HololibLibraryLocation() string { + return ensureDirectory(filepath.Join(HololibLocation(), "library")) +} + func HolotreeLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "holotree")) } diff --git a/common/version.go b/common/version.go index 39a0b86e..bf6101d5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.16.0` + Version = `v9.17.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2e3acaab..65292495 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.17.0 (date: 26.5.2021) + +- added `export` command to holotree [experimental] + ## v9.16.0 (date: 21.5.2021) - catalog extension based on operating system, architecture and directory diff --git a/htfs/functions.go b/htfs/functions.go index 85b3de6f..7415ec00 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/trollhash" ) @@ -320,3 +321,29 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat } } } + +type Zipper interface { + Add(fullpath, relativepath string) error +} + +func ZipRoot(library Library, fs *Root, sink Zipper) Treetop { + var tool Treetop + baseline := common.HololibLocation() + tool = func(path string, it *Dir) (err error) { + defer fail.Around(&err) + + for _, file := range it.Files { + location := library.ExactLocation(file.Digest) + relative, err := filepath.Rel(baseline, location) + fail.On(err != nil, "Relative path error: %s -> %s -> %v", baseline, location, err) + err = sink.Add(location, relative) + fail.On(err != nil, "%v", err) + } + for name, subdir := range it.Dirs { + err := tool(filepath.Join(path, name), subdir) + fail.On(err != nil, "%v", err) + } + return nil + } + return tool +} diff --git a/htfs/library.go b/htfs/library.go index 2d8175a0..b3af1c4d 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -1,10 +1,13 @@ package htfs import ( + "archive/zip" "fmt" + "io" "os" "path/filepath" "runtime" + "sort" "strings" "sync" "time" @@ -13,6 +16,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" ) @@ -43,6 +47,7 @@ func (it *stats) Dirty(dirty bool) { type Library interface { Identity() string Stage() string + Export([]string, string) error Record([]byte) error Restore([]byte, []byte, []byte) (string, error) Location(string) string @@ -57,11 +62,11 @@ type hololib struct { } func (it *hololib) Location(digest string) string { - return filepath.Join(common.HololibLocation(), "library", digest[:2], digest[2:4], digest[4:6]) + return filepath.Join(common.HololibLibraryLocation(), digest[:2], digest[2:4], digest[4:6]) } func (it *hololib) ExactLocation(digest string) string { - return filepath.Join(common.HololibLocation(), "library", digest[:2], digest[2:4], digest[4:6], digest) + return filepath.Join(common.HololibLibraryLocation(), digest[:2], digest[2:4], digest[4:6], digest) } func (it *hololib) Identity() string { @@ -77,6 +82,62 @@ func (it *hololib) Stage() string { return stage } +type zipseen struct { + *zip.Writer + seen map[string]bool +} + +func (it zipseen) Add(fullpath, relativepath string) (err error) { + defer fail.Around(&err) + + if it.seen[relativepath] { + return nil + } + it.seen[relativepath] = true + + source, err := os.Open(fullpath) + fail.On(err != nil, "Could not open: %q -> %v", fullpath, err) + defer source.Close() + target, err := it.Create(relativepath) + fail.On(err != nil, "Could not create: %q -> %v", relativepath, err) + _, err = io.Copy(target, source) + fail.On(err != nil, "Copy failure: %q -> %q -> %v", fullpath, relativepath, err) + return nil +} + +func (it *hololib) Export(catalogs []string, archive string) (err error) { + defer fail.Around(&err) + + common.Timeline("holotree export start") + defer common.Timeline("holotree export done") + + handle, err := os.Create(archive) + fail.On(err != nil, "Could not create archive %q.", archive) + writer := zip.NewWriter(handle) + defer writer.Close() + + zipper := &zipseen{ + writer, + make(map[string]bool), + } + + for _, name := range catalogs { + catalog := filepath.Join(common.HololibCatalogLocation(), name) + relative, err := filepath.Rel(common.HololibLocation(), catalog) + fail.On(err != nil, "Could not get relative location for catalog -> %v.", err) + err = zipper.Add(catalog, relative) + fail.On(err != nil, "Could not add catalog to zip -> %v.", err) + + fs, err := NewRoot(".") + fail.On(err != nil, "Could not create root location -> %v.", err) + err = fs.LoadFrom(catalog) + fail.On(err != nil, "Could not load catalog from %s -> %v.", catalog, err) + err = fs.Treetop(ZipRoot(it, fs, zipper)) + fail.On(err != nil, "Could not zip catalog %s -> %v.", catalog, err) + } + return nil +} + func (it *hololib) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() key := BlueprintHash(blueprint) @@ -112,7 +173,7 @@ func (it *hololib) Record(blueprint []byte) error { func (it *hololib) CatalogPath(key string) string { name := fmt.Sprintf("%s.%016x", key, it.identity) - return filepath.Join(common.HololibLocation(), "catalog", name) + return filepath.Join(common.HololibCatalogLocation(), name) } func (it *hololib) HasBlueprint(blueprint []byte) bool { @@ -152,6 +213,15 @@ func (it *hololib) queryBlueprint(key string) bool { return pathlib.IsFile(catalog) } +func Catalogs() []string { + result := make([]string, 0, 10) + for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*.[0-9a-f]*") { + result = append(result, catalog) + } + sort.Strings(result) + return result +} + func Spacemap() map[string]string { result := make(map[string]string) basedir := common.HolotreeLocation() @@ -303,6 +373,10 @@ func (it *virtual) Stage() string { return stage } +func (it *virtual) Export([]string, string) error { + return fmt.Errorf("Not supported yet on virtual holotree.") +} + func (it *virtual) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() key := BlueprintHash(blueprint) From f2fe66db491636e082f697a53aa96f3adac243ee Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Jun 2021 13:57:26 +0300 Subject: [PATCH 139/516] RCC-180: exporting catalogs from holotree (v9.17.1) - adding supporting structures for zip based holotree runs [experimental] --- cmd/cloudPrepare.go | 2 +- cmd/holotreeVariables.go | 6 +- common/version.go | 2 +- docs/changelog.md | 4 + htfs/commands.go | 24 ++++-- htfs/delegates.go | 31 +++++++ htfs/directory.go | 26 ++++-- htfs/fs_test.go | 12 +++ htfs/functions.go | 28 ++----- htfs/library.go | 167 ++++++-------------------------------- htfs/testdata/simple.yaml | 4 + htfs/testdata/simple.zip | Bin 0 -> 156700 bytes htfs/virtual.go | 126 ++++++++++++++++++++++++++++ htfs/ziplibrary.go | 121 +++++++++++++++++++++++++++ operations/running.go | 2 +- operations/zipper.go | 5 +- pathlib/walk.go | 27 ++++-- robot/robot.go | 9 ++ 18 files changed, 412 insertions(+), 184 deletions(-) create mode 100644 htfs/delegates.go create mode 100644 htfs/testdata/simple.yaml create mode 100644 htfs/testdata/simple.zip create mode 100644 htfs/virtual.go create mode 100644 htfs/ziplibrary.go diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 0ef58b64..e909ef72 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -57,7 +57,7 @@ var prepareCloudCmd = &cobra.Command{ var label string condafile := config.CondaConfigFile() if len(common.HolotreeSpace) > 0 { - label, err = htfs.NewEnvironment(false, condafile) + label, err = htfs.NewEnvironment(false, condafile, config.Holozip()) } else { label, err = conda.NewEnvironment(false, condafile) } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 62b76922..67c3f3bb 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -31,7 +31,11 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp err = os.WriteFile(condafile, holotreeBlueprint, 0o640) pretty.Guard(err == nil, 6, "%s", err) - path, err := htfs.NewEnvironment(force, condafile) + holozip := "" + if config != nil { + holozip = config.Holozip() + } + path, err := htfs.NewEnvironment(force, condafile, holozip) pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/common/version.go b/common/version.go index bf6101d5..0b9453d1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.17.0` + Version = `v9.17.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 65292495..a4566a0d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.17.1 (date: 2.6.2021) + +- adding supporting structures for zip based holotree runs [experimental] + ## v9.17.0 (date: 26.5.2021) - added `export` command to holotree [experimental] diff --git a/htfs/commands.go b/htfs/commands.go index 8437e4da..a4bba96d 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -13,9 +13,11 @@ import ( "github.com/robocorp/rcc/robot" ) -func NewEnvironment(force bool, condafile string) (label string, err error) { +func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) + haszip := len(holozip) > 0 + common.Timeline("new holotree environment") _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) @@ -26,19 +28,27 @@ func NewEnvironment(force bool, condafile string) (label string, err error) { fail.On(err != nil, "%s", err) common.Timeline("holotree library created") - if !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { + if !haszip && !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { tree = Virtual() common.Timeline("downgraded to virtual holotree library") } - err = RecordEnvironment(tree, holotreeBlueprint, force) - fail.On(err != nil, "%s", err) + var library Library + if haszip { + library, err = ZipLibrary(holozip) + fail.On(err != nil, "Failed to load %q -> %s", holozip, err) + common.Timeline("downgraded to holotree zip library") + } else { + err = RecordEnvironment(tree, holotreeBlueprint, force) + fail.On(err != nil, "%s", err) + library = tree + } - path, err := tree.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) return path, nil } -func RecordCondaEnvironment(tree Library, condafile string, force bool) (err error) { +func RecordCondaEnvironment(tree MutableLibrary, condafile string, force bool) (err error) { defer fail.Around(&err) right, err := conda.ReadCondaYaml(condafile) @@ -50,7 +60,7 @@ func RecordCondaEnvironment(tree Library, condafile string, force bool) (err err return RecordEnvironment(tree, []byte(content), force) } -func RecordEnvironment(tree Library, blueprint []byte, force bool) (err error) { +func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err error) { defer fail.Around(&err) // following must be setup here diff --git a/htfs/delegates.go b/htfs/delegates.go new file mode 100644 index 00000000..c1eaf48a --- /dev/null +++ b/htfs/delegates.go @@ -0,0 +1,31 @@ +package htfs + +import ( + "compress/gzip" + "io" + "os" + + "github.com/robocorp/rcc/fail" +) + +func delegateOpen(it MutableLibrary, digest string) (readable io.Reader, closer Closer, err error) { + defer fail.Around(&err) + + filename := it.ExactLocation(digest) + source, err := os.Open(filename) + fail.On(err != nil, "Failed to open %q -> %v", filename, err) + + var reader io.ReadCloser + reader, err = gzip.NewReader(source) + if err != nil { + _, err = source.Seek(0, 0) + fail.On(err != nil, "Failed to seek %q -> %v", filename, err) + reader = source + } + closer = func() error { + reader.Close() + source.Sync() + return source.Close() + } + return reader, closer, nil +} diff --git a/htfs/directory.go b/htfs/directory.go index 17993eb1..06ff3c40 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -4,9 +4,12 @@ import ( "compress/gzip" "encoding/json" "fmt" + "io" "io/fs" "os" "path/filepath" + "runtime" + "strings" "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" @@ -36,6 +39,7 @@ type Root struct { Path string `json:"path"` Controller string `json:"controller"` Space string `json:"space"` + Platform string `json:"platform"` Blueprint string `json:"blueprint"` Lifted bool `json:"lifted"` Tree *Dir `json:"tree"` @@ -50,20 +54,28 @@ func NewRoot(path string) (*Root, error) { return &Root{ Identity: basename, Path: fullpath, + Platform: strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)), Lifted: false, Tree: newDir(""), }, nil } +func (it *Root) HolotreeBase() string { + return filepath.Dir(it.Path) +} + +func (it *Root) Signature() uint64 { + return sipit([]byte(strings.ToLower(fmt.Sprintf("%s %q", it.Platform, it.Path)))) +} + func (it *Root) Rewrite() []byte { return []byte(it.Identity) } func (it *Root) Relocate(target string) error { - origin := filepath.Dir(it.Path) locate := filepath.Dir(target) - if origin != locate { - return fmt.Errorf("Base directory mismatch: %q vs %q.", origin, locate) + if it.HolotreeBase() != locate { + return fmt.Errorf("Base directory mismatch: %q vs %q.", it.HolotreeBase(), locate) } basename := filepath.Base(target) if len(it.Identity) != len(basename) { @@ -133,6 +145,11 @@ func (it *Root) SaveAs(filename string) error { return nil } +func (it *Root) ReadFrom(source io.Reader) error { + decoder := json.NewDecoder(source) + return decoder.Decode(&it) +} + func (it *Root) LoadFrom(filename string) error { common.Timeline("holotree load %q", filename) defer common.Timeline("holotree load done") @@ -146,8 +163,7 @@ func (it *Root) LoadFrom(filename string) error { return err } defer reader.Close() - decoder := json.NewDecoder(reader) - return decoder.Decode(&it) + return it.ReadFrom(reader) } type Dir struct { diff --git a/htfs/fs_test.go b/htfs/fs_test.go index 46db9233..4c21a008 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -41,3 +41,15 @@ func TestHTFSspecification(t *testing.T) { must.Equal(len(after), len(content)) must.Equal(fs.Path, reloaded.Path) } + +func TestZipLibrary(t *testing.T) { + must, wont := hamlet.Specifications(t) + + _, blueprint, err := htfs.ComposeFinalBlueprint([]string{"testdata/simple.yaml"}, "") + must.Nil(err) + wont.Nil(blueprint) + sut, err := htfs.ZipLibrary("testdata/simple.zip") + must.Nil(err) + wont.Nil(sut) + must.True(sut.HasBlueprint(blueprint)) +} diff --git a/htfs/functions.go b/htfs/functions.go index 7415ec00..d2fbbe31 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -15,7 +15,7 @@ import ( "github.com/robocorp/rcc/trollhash" ) -func CatalogCheck(library Library, fs *Root) Treetop { +func CatalogCheck(library MutableLibrary, fs *Root) Treetop { var tool Treetop tool = func(path string, it *Dir) error { for name, file := range it.Files { @@ -100,7 +100,7 @@ func MakeBranches(path string, it *Dir) error { return os.Chtimes(path, motherTime, motherTime) } -func ScheduleLifters(library Library, stats *stats) Treetop { +func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { var scheduler Treetop scheduler = func(path string, it *Dir) error { for name, subdir := range it.Dirs { @@ -173,23 +173,13 @@ func LiftFlatFile(sourcename, sinkname string) anywork.Work { } } -func DropFile(sourcename, sinkname string, details *File, rewrite []byte) anywork.Work { +func DropFile(library Library, digest, sinkname string, details *File, rewrite []byte) anywork.Work { return func() { - source, err := os.Open(sourcename) + reader, closer, err := library.Open(digest) if err != nil { panic(err) } - defer source.Close() - var reader io.ReadCloser - reader, err = gzip.NewReader(source) - if err != nil { - _, err = source.Seek(0, 0) - if err != nil { - panic(err) - } - reader = source - } - defer reader.Close() + defer closer() sink, err := os.Create(sinkname) if err != nil { panic(err) @@ -304,8 +294,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat stats.Dirty(!ok) if !ok { common.Debug("* Holotree: update changed file %q", directpath) - droppath := library.ExactLocation(found.Digest) - anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) + anywork.Backlog(DropFile(library, found.Digest, directpath, found, fs.Rewrite())) } } for name, found := range it.Files { @@ -314,8 +303,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat if !seen { stats.Dirty(true) common.Debug("* Holotree: add missing file %q", directpath) - droppath := library.ExactLocation(found.Digest) - anywork.Backlog(DropFile(droppath, directpath, found, fs.Rewrite())) + anywork.Backlog(DropFile(library, found.Digest, directpath, found, fs.Rewrite())) } } } @@ -326,7 +314,7 @@ type Zipper interface { Add(fullpath, relativepath string) error } -func ZipRoot(library Library, fs *Root, sink Zipper) Treetop { +func ZipRoot(library MutableLibrary, fs *Root, sink Zipper) Treetop { var tool Treetop baseline := common.HololibLocation() tool = func(path string, it *Dir) (err error) { diff --git a/htfs/library.go b/htfs/library.go index b3af1c4d..80b63eb7 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -44,15 +44,23 @@ func (it *stats) Dirty(dirty bool) { } } +type Closer func() error + type Library interface { + HasBlueprint([]byte) bool + Open(string) (io.Reader, Closer, error) + Restore([]byte, []byte, []byte) (string, error) +} + +type MutableLibrary interface { + Library + Identity() string - Stage() string + ExactLocation(string) string Export([]string, string) error - Record([]byte) error - Restore([]byte, []byte, []byte) (string, error) Location(string) string - ExactLocation(string) string - HasBlueprint([]byte) bool + Record([]byte) error + Stage() string } type hololib struct { @@ -61,6 +69,10 @@ type hololib struct { queryCache map[string]bool } +func (it *hololib) Open(digest string) (readable io.Reader, closer Closer, err error) { + return delegateOpen(it, digest) +} + func (it *hololib) Location(digest string) string { return filepath.Join(common.HololibLibraryLocation(), digest[:2], digest[2:4], digest[4:6]) } @@ -248,7 +260,8 @@ func Spaces() []*Root { return roots } -func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { +func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { + defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) common.Timeline("holotree restore start %s", key) @@ -268,38 +281,26 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (string, error) { common.Timeline("holotree digest done") } fs, err := NewRoot(it.Stage()) - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to create stage -> %v", err) err = fs.LoadFrom(it.CatalogPath(key)) - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to load catalog %s -> %v", it.CatalogPath(key), err) err = fs.Relocate(targetdir) - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to relocate %s -> %v", targetdir, err) common.Timeline("holotree make branches start") err = fs.Treetop(MakeBranches) common.Timeline("holotree make branches done") - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to make branches -> %v", err) score := &stats{} common.Timeline("holotree restore start") err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to restore directories -> %v", err) common.Timeline("holotree restore done") defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) fs.Controller = string(client) fs.Space = string(tag) err = fs.SaveAs(metafile) - if err != nil { - return "", err - } + fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) return targetdir, nil } @@ -333,7 +334,7 @@ func makedirs(prefix string, suffixes ...string) error { return nil } -func New() (Library, error) { +func New() (MutableLibrary, error) { err := makedirs(common.HololibLocation(), "library", "catalog") if err != nil { return nil, err @@ -346,119 +347,3 @@ func New() (Library, error) { queryCache: make(map[string]bool), }, nil } - -type virtual struct { - identity uint64 - root *Root - registry map[string]string - key string -} - -func Virtual() Library { - return &virtual{ - identity: sipit([]byte(common.RobocorpHome())), - } -} - -func (it *virtual) Identity() string { - return fmt.Sprintf("v%016xh", it.identity) -} - -func (it *virtual) Stage() string { - stage := filepath.Join(common.HolotreeLocation(), it.Identity()) - err := os.MkdirAll(stage, 0o755) - if err != nil { - panic(err) - } - return stage -} - -func (it *virtual) Export([]string, string) error { - return fmt.Errorf("Not supported yet on virtual holotree.") -} - -func (it *virtual) Record(blueprint []byte) error { - defer common.Stopwatch("Holotree recording took:").Debug() - key := BlueprintHash(blueprint) - common.Timeline("holotree record start %s (virtual)", key) - fs, err := NewRoot(it.Stage()) - if err != nil { - return err - } - err = fs.Lift() - if err != nil { - return err - } - common.Timeline("holotree (re)locator start (virtual)") - err = fs.AllFiles(Locator(it.Identity())) - if err != nil { - return err - } - common.Timeline("holotree (re)locator done (virtual)") - it.registry = make(map[string]string) - fs.Treetop(DigestMapper(it.registry)) - fs.Blueprint = key - it.root = fs - it.key = key - return nil -} - -func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { - defer common.Stopwatch("Holotree restore took:").Debug() - key := BlueprintHash(blueprint) - common.Timeline("holotree restore start %s (virtual)", key) - prefix := textual(sipit(client), 9) - suffix := textual(sipit(tag), 8) - name := prefix + "_" + suffix - metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(common.HolotreeLocation(), name) - currentstate := make(map[string]string) - shadow, err := NewRoot(targetdir) - if err == nil { - err = shadow.LoadFrom(metafile) - } - if err == nil { - common.Timeline("holotree digest start (virtual)") - shadow.Treetop(DigestRecorder(currentstate)) - common.Timeline("holotree digest done (virtual)") - } - fs := it.root - err = fs.Relocate(targetdir) - if err != nil { - return "", err - } - common.Timeline("holotree make branches start (virtual)") - err = fs.Treetop(MakeBranches) - common.Timeline("holotree make branches done (virtual)") - if err != nil { - return "", err - } - score := &stats{} - common.Timeline("holotree restore start (virtual)") - err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) - if err != nil { - return "", err - } - common.Timeline("holotree restore done (virtual)") - defer common.Timeline("- dirty %d/%d", score.dirty, score.total) - common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) - fs.Controller = string(client) - fs.Space = string(tag) - err = fs.SaveAs(metafile) - if err != nil { - return "", err - } - return targetdir, nil -} - -func (it *virtual) ExactLocation(key string) string { - return it.registry[key] -} - -func (it *virtual) Location(key string) string { - panic("Location is not supported on virtual holotree.") -} - -func (it *virtual) HasBlueprint(blueprint []byte) bool { - return it.key == BlueprintHash(blueprint) -} diff --git a/htfs/testdata/simple.yaml b/htfs/testdata/simple.yaml new file mode 100644 index 00000000..9c46ef58 --- /dev/null +++ b/htfs/testdata/simple.yaml @@ -0,0 +1,4 @@ +channels: + - conda-forge +dependencies: + - ca-certificates diff --git a/htfs/testdata/simple.zip b/htfs/testdata/simple.zip new file mode 100644 index 0000000000000000000000000000000000000000..ee3f96ba1f5caed9952880cefffd29ff08db009b GIT binary patch literal 156700 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 z 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 DfTdq< literal 0 HcmV?d00001 diff --git a/htfs/virtual.go b/htfs/virtual.go new file mode 100644 index 00000000..8cc5c3cf --- /dev/null +++ b/htfs/virtual.go @@ -0,0 +1,126 @@ +package htfs + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" +) + +type virtual struct { + identity uint64 + root *Root + registry map[string]string + key string +} + +func Virtual() MutableLibrary { + return &virtual{ + identity: sipit([]byte(common.RobocorpHome())), + } +} + +func (it *virtual) Identity() string { + return fmt.Sprintf("v%016xh", it.identity) +} + +func (it *virtual) Stage() string { + stage := filepath.Join(common.HolotreeLocation(), it.Identity()) + err := os.MkdirAll(stage, 0o755) + if err != nil { + panic(err) + } + return stage +} + +func (it *virtual) Export([]string, string) error { + return fmt.Errorf("Not supported yet on virtual holotree.") +} + +func (it *virtual) Record(blueprint []byte) (err error) { + defer fail.Around(&err) + defer common.Stopwatch("Holotree recording took:").Debug() + key := BlueprintHash(blueprint) + common.Timeline("holotree record start %s (virtual)", key) + fs, err := NewRoot(it.Stage()) + fail.On(err != nil, "Failed to create stage root: %v", err) + err = fs.Lift() + fail.On(err != nil, "Failed to lift structure out of stage: %v", err) + common.Timeline("holotree (re)locator start (virtual)") + err = fs.AllFiles(Locator(it.Identity())) + fail.On(err != nil, "Failed to apply relocate to stage: %v", err) + common.Timeline("holotree (re)locator done (virtual)") + it.registry = make(map[string]string) + fs.Treetop(DigestMapper(it.registry)) + fs.Blueprint = key + it.root = fs + it.key = key + return nil +} + +func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { + defer common.Stopwatch("Holotree restore took:").Debug() + key := BlueprintHash(blueprint) + common.Timeline("holotree restore start %s (virtual)", key) + prefix := textual(sipit(client), 9) + suffix := textual(sipit(tag), 8) + name := prefix + "_" + suffix + metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(common.HolotreeLocation(), name) + currentstate := make(map[string]string) + shadow, err := NewRoot(targetdir) + if err == nil { + err = shadow.LoadFrom(metafile) + } + if err == nil { + common.Timeline("holotree digest start (virtual)") + shadow.Treetop(DigestRecorder(currentstate)) + common.Timeline("holotree digest done (virtual)") + } + fs := it.root + err = fs.Relocate(targetdir) + if err != nil { + return "", err + } + common.Timeline("holotree make branches start (virtual)") + err = fs.Treetop(MakeBranches) + common.Timeline("holotree make branches done (virtual)") + if err != nil { + return "", err + } + score := &stats{} + common.Timeline("holotree restore start (virtual)") + err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + if err != nil { + return "", err + } + common.Timeline("holotree restore done (virtual)") + defer common.Timeline("- dirty %d/%d", score.dirty, score.total) + common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + fs.Controller = string(client) + fs.Space = string(tag) + err = fs.SaveAs(metafile) + if err != nil { + return "", err + } + return targetdir, nil +} + +func (it *virtual) Open(digest string) (readable io.Reader, closer Closer, err error) { + return delegateOpen(it, digest) +} + +func (it *virtual) ExactLocation(key string) string { + return it.registry[key] +} + +func (it *virtual) Location(key string) string { + panic("Location is not supported on virtual holotree.") +} + +func (it *virtual) HasBlueprint(blueprint []byte) bool { + return it.key == BlueprintHash(blueprint) +} diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go new file mode 100644 index 00000000..3f8b4264 --- /dev/null +++ b/htfs/ziplibrary.go @@ -0,0 +1,121 @@ +package htfs + +import ( + "archive/zip" + "compress/gzip" + "fmt" + "io" + "path/filepath" + "runtime" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" +) + +type ziplibrary struct { + content *zip.ReadCloser + identity uint64 + root *Root + lookup map[string]*zip.File +} + +func ZipLibrary(zipfile string) (Library, error) { + content, err := zip.OpenReader(zipfile) + if err != nil { + return nil, err + } + lookup := make(map[string]*zip.File) + for _, entry := range content.File { + lookup[entry.Name] = entry + } + identity := strings.ToLower(fmt.Sprintf("%s %s %q", runtime.GOOS, runtime.GOARCH, common.RobocorpHome())) + return &ziplibrary{ + content: content, + identity: sipit([]byte(identity)), + lookup: lookup, + }, nil +} + +func (it *ziplibrary) HasBlueprint(blueprint []byte) bool { + key := BlueprintHash(blueprint) + _, ok := it.lookup[it.CatalogPath(key)] + return ok +} + +func (it *ziplibrary) openFile(filename string) (readable io.Reader, closer Closer, err error) { + content, ok := it.lookup[filename] + if !ok { + return nil, nil, fmt.Errorf("Missing file: %q", filename) + } + file, err := content.Open() + if err != nil { + return nil, nil, err + } + wrapper, err := gzip.NewReader(file) + if err != nil { + return nil, nil, err + } + closer = func() error { + wrapper.Close() + return file.Close() + } + return wrapper, closer, nil +} + +func (it *ziplibrary) Open(digest string) (readable io.Reader, closer Closer, err error) { + filename := filepath.Join("library", digest[:2], digest[2:4], digest[4:6], digest) + return it.openFile(filename) +} + +func (it *ziplibrary) CatalogPath(key string) string { + return filepath.Join("catalog", fmt.Sprintf("%s.%016x", key, it.identity)) +} + +func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { + defer fail.Around(&err) + defer common.Stopwatch("Holotree restore took:").Debug() + key := BlueprintHash(blueprint) + common.Timeline("holotree restore start %s (zip)", key) + prefix := textual(sipit(client), 9) + suffix := textual(sipit(tag), 8) + name := prefix + "_" + suffix + fs, err := NewRoot(".") + fail.On(err != nil, "Failed to create root -> %v", err) + catalog := it.CatalogPath(key) + reader, closer, err := it.openFile(catalog) + fail.On(err != nil, "Failed to open catalog %q -> %v", catalog, err) + defer closer() + err = fs.ReadFrom(reader) + fail.On(err != nil, "Failed to read catalog %q -> %v", catalog, err) + metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(fs.HolotreeBase(), name) + currentstate := make(map[string]string) + shadow, err := NewRoot(targetdir) + if err == nil { + err = shadow.LoadFrom(metafile) + } + if err == nil { + common.Timeline("holotree digest start (zip)") + shadow.Treetop(DigestRecorder(currentstate)) + common.Timeline("holotree digest done (zip)") + } + err = fs.Relocate(targetdir) + fail.On(err != nil, "Failed to relocate %q -> %v", targetdir, err) + common.Timeline("holotree make branches start (zip)") + err = fs.Treetop(MakeBranches) + common.Timeline("holotree make branches done (zip)") + fail.On(err != nil, "Failed to make branches %q -> %v", targetdir, err) + score := &stats{} + common.Timeline("holotree restore start (zip)") + err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) + fail.On(err != nil, "Failed to restore directory %q -> %v", targetdir, err) + common.Timeline("holotree restore done (zip)") + defer common.Timeline("- dirty %d/%d", score.dirty, score.total) + common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + fs.Controller = string(client) + fs.Space = string(tag) + err = fs.SaveAs(metafile) + fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) + return targetdir, nil +} diff --git a/operations/running.go b/operations/running.go index dd008112..e8e6947c 100644 --- a/operations/running.go +++ b/operations/running.go @@ -87,7 +87,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. var label string if len(common.HolotreeSpace) > 0 { - label, err = htfs.NewEnvironment(force, config.CondaConfigFile()) + label, err = htfs.NewEnvironment(force, config.CondaConfigFile(), config.Holozip()) } else { label, err = conda.NewEnvironment(force, config.CondaConfigFile()) } diff --git a/operations/zipper.go b/operations/zipper.go index 80464e32..a1cb305b 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -265,6 +265,7 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { func Unzip(directory, zipfile string, force, temporary bool) error { common.Timeline("unzip %q to %q", zipfile, directory) + defer common.Timeline("unzip done") fullpath, err := filepath.Abs(directory) if err != nil { return err @@ -297,6 +298,8 @@ func Unzip(directory, zipfile string, force, temporary bool) error { } func Zip(directory, zipfile string, ignores []string) error { + common.Timeline("zip %q to %q", directory, zipfile) + defer common.Timeline("zip done") common.Debug("Wrapping %v into %v ...", directory, zipfile) config, err := robot.LoadRobotYaml(robot.DetectConfigurationName(directory), false) if err != nil { @@ -313,6 +316,6 @@ func Zip(directory, zipfile string, ignores []string) error { return err } defaults := defaultIgnores(zipfile) - pathlib.Walk(directory, pathlib.CompositeIgnore(defaults, ignored), zipper.Add) + pathlib.ForceWalk(directory, pathlib.ForceFilename("hololib.zip"), pathlib.CompositeIgnore(defaults, ignored), zipper.Add) return nil } diff --git a/pathlib/walk.go b/pathlib/walk.go index b9f6a13b..6706813c 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -9,6 +9,7 @@ import ( "time" ) +type Forced func(os.FileInfo) bool type Ignore func(os.FileInfo) bool type Report func(string, string, os.FileInfo) @@ -32,6 +33,16 @@ func IgnoreDirectories(target os.FileInfo) bool { return target.IsDir() } +func ForceNothing(_ os.FileInfo) bool { + return false +} + +func ForceFilename(filename string) Forced { + return func(target os.FileInfo) bool { + return !target.IsDir() && target.Name() == filename + } +} + func NoReporting(string, string, os.FileInfo) { } @@ -122,20 +133,20 @@ func folderEntries(directory string) ([]os.FileInfo, error) { return entries, nil } -func recursiveWalk(directory, prefix string, ignore Ignore, report Report) error { +func recursiveWalk(directory, prefix string, force Forced, ignore Ignore, report Report) error { entries, err := folderEntries(directory) if err != nil { return err } sorted(entries) for _, entry := range entries { - if ignore(entry) { + if !force(entry) && ignore(entry) { continue } nextPrefix := filepath.Join(prefix, entry.Name()) entryPath := filepath.Join(directory, entry.Name()) if entry.IsDir() { - recursiveWalk(entryPath, nextPrefix, ignore, report) + recursiveWalk(entryPath, nextPrefix, force, ignore, report) } else { report(entryPath, nextPrefix, entry) } @@ -143,12 +154,16 @@ func recursiveWalk(directory, prefix string, ignore Ignore, report Report) error return nil } -func Walk(directory string, ignore Ignore, report Report) error { +func ForceWalk(directory string, force Forced, ignore Ignore, report Report) error { fullpath, err := filepath.Abs(directory) if err != nil { return err } - return recursiveWalk(fullpath, ".", ignore, report) + return recursiveWalk(fullpath, ".", force, ignore, report) +} + +func Walk(directory string, ignore Ignore, report Report) error { + return ForceWalk(directory, ForceNothing, ignore, report) } func Glob(directory string, pattern string) []string { @@ -160,6 +175,6 @@ func Glob(directory string, pattern string) []string { capture := func(_, localpath string, _ os.FileInfo) { result = append(result, localpath) } - Walk(directory, ignore, capture) + ForceWalk(directory, ForceNothing, ignore, capture) return result } diff --git a/robot/robot.go b/robot/robot.go index a3aed8d8..414f65fe 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -27,6 +27,7 @@ type Robot interface { CondaConfigFile() string CondaHash() string RootDirectory() string + Holozip() string Validate() (bool, error) Diagnostics(*common.DiagnosticStatus, bool) @@ -204,6 +205,14 @@ func (it *robot) RootDirectory() string { return it.Root } +func (it *robot) Holozip() string { + zippath := filepath.Join(it.Root, "hololib.zip") + if pathlib.IsFile(zippath) { + return zippath + } + return "" +} + func (it *robot) IgnoreFiles() []string { if it.Ignored == nil { return []string{} From f3a88b0ec12a75fc3bc216ef01f7a1f8b068d64e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Jun 2021 16:11:56 +0300 Subject: [PATCH 140/516] RCC-180: exporting catalogs from holotree (v9.17.2) - fixing broken tests, and taking account changed specifications --- common/version.go | 2 +- docs/changelog.md | 6 +++++- htfs/commands.go | 6 ++++++ htfs/directory.go | 3 +-- htfs/fs_test.go | 2 ++ htfs/library.go | 6 +++--- htfs/testdata/simple.zip | Bin 156700 -> 154134 bytes htfs/ziplibrary.go | 4 ++-- 8 files changed, 20 insertions(+), 9 deletions(-) diff --git a/common/version.go b/common/version.go index 0b9453d1..0bde312b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.17.1` + Version = `v9.17.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index a4566a0d..cfd1601d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v9.17.1 (date: 2.6.2021) +## v9.17.2 (date: 2.6.2021) + +- fixing broken tests, and taking account changed specifications + +## v9.17.1 (date: 2.6.2021) broken - adding supporting structures for zip based holotree runs [experimental] diff --git a/htfs/commands.go b/htfs/commands.go index a4bba96d..a3440e0e 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -1,9 +1,11 @@ package htfs import ( + "fmt" "io/ioutil" "os" "path/filepath" + "runtime" "strings" "github.com/robocorp/rcc/anywork" @@ -13,6 +15,10 @@ import ( "github.com/robocorp/rcc/robot" ) +func Platform() string { + return strings.ToLower(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) +} + func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) diff --git a/htfs/directory.go b/htfs/directory.go index 06ff3c40..f31002a1 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -8,7 +8,6 @@ import ( "io/fs" "os" "path/filepath" - "runtime" "strings" "github.com/robocorp/rcc/anywork" @@ -54,7 +53,7 @@ func NewRoot(path string) (*Root, error) { return &Root{ Identity: basename, Path: fullpath, - Platform: strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)), + Platform: Platform(), Lifted: false, Tree: newDir(""), }, nil diff --git a/htfs/fs_test.go b/htfs/fs_test.go index 4c21a008..2c66b41a 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -45,6 +45,8 @@ func TestHTFSspecification(t *testing.T) { func TestZipLibrary(t *testing.T) { must, wont := hamlet.Specifications(t) + must.Equal("linux_amd64", htfs.Platform()) + _, blueprint, err := htfs.ComposeFinalBlueprint([]string{"testdata/simple.yaml"}, "") must.Nil(err) wont.Nil(blueprint) diff --git a/htfs/library.go b/htfs/library.go index 80b63eb7..14d7d85e 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -184,7 +184,7 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - name := fmt.Sprintf("%s.%016x", key, it.identity) + name := fmt.Sprintf("%s.%s", key, Platform()) return filepath.Join(common.HololibCatalogLocation(), name) } @@ -227,7 +227,7 @@ func (it *hololib) queryBlueprint(key string) bool { func Catalogs() []string { result := make([]string, 0, 10) - for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*.[0-9a-f]*") { + for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*.*") { result = append(result, catalog) } sort.Strings(result) @@ -340,7 +340,7 @@ func New() (MutableLibrary, error) { return nil, err } basedir := common.RobocorpHome() - identity := strings.ToLower(fmt.Sprintf("%s %s %q", runtime.GOOS, runtime.GOARCH, basedir)) + identity := strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)) return &hololib{ identity: sipit([]byte(identity)), basedir: basedir, diff --git a/htfs/testdata/simple.zip b/htfs/testdata/simple.zip index ee3f96ba1f5caed9952880cefffd29ff08db009b..bfba0b7b44e3db7951e16a4b78bd231a26417d43 100644 GIT binary patch literal 154134 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%0DU0ncSZ}5 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 +## Why use rcc? + +* Are developers manually installing conda or pip packages? Here rcc makes it easier for developers to just worry about getting `conda.yaml` and `robot.yaml` right, and then let rcc to do the heavy lifting of keeping environments pristine, clean, and up to date. +* Have you run into "works on my machine" problem, where the original developer has a working setup, but others have a hard time repeating the experience? In this case, let rcc help you to set up repeatable runtime environments across users and operating systems. +* Have you experienced "configuration drift", where once working runtime environment dependencies get updated and break your production system? Here rcc can help by either making drift visible or freezing all dependencies so that drifting does not happen. +* Do you have python programs that have conflicting dependencies? There rcc can help by making dedicated runtime environments for different setups, where different `robot.yaml` files define what to run and `conda.yaml` defines runtime environment dependencies + + ## Getting Started :arrow_double_down: Install rcc diff --git a/cmd/holotreePlan.go b/cmd/holotreePlan.go index 520e932d..e3b04d65 100644 --- a/cmd/holotreePlan.go +++ b/cmd/holotreePlan.go @@ -12,11 +12,13 @@ import ( ) var holotreePlanCmd = &cobra.Command{ - Use: "plan", + Use: "plan ", Short: "Show installation plans for given holotree spaces (or substrings)", Long: "Show installation plans for given holotree spaces (or substrings)", + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + found := false for _, prefix := range args { for _, label := range htfs.FindEnvironment(prefix) { planfile, ok := htfs.InstallationPlan(label) @@ -24,8 +26,11 @@ var holotreePlanCmd = &cobra.Command{ content, err := ioutil.ReadFile(planfile) pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) fmt.Fprintf(os.Stdout, string(content)) + found = true } } + pretty.Guard(found, 3, "Nothing matched given plans!") + pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index 4cad3a07..8a663126 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.8` + Version = `v11.1.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index f2371943..03d021bd 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.2/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.15.3/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 22766251..f83d9294 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.2/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.15.3/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 8c00f0ce..a0305180 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.2/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.15.3/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 2da2304d..be41cb83 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -155,7 +155,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 15002 + goodEnough := version >= 15003 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index 20bf96b6..99e0a825 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,12 @@ # rcc change log -## v11.0.8 (date: 14.9.2021) UNSTABLE +## v11.1.0 (date: 16.9.2021) + +- BREAKING CHANGES, but now this may be considered stable(ish) +- micromamba update to version 0.15.3 +- added more robot tests and improved `rcc holotree plan` command + +## v11.0.8 (date: 15.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - showing correct `rcc_plan.log` and `identity.yaml` files on log diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index d3ec4be7..2ee3fe47 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -16,7 +16,7 @@ Prepare Local Set Environment Variable ROBOCORP_HOME tmp/robocorp Comment Verify micromamba is installed or download and install it. - Step build/rcc ht vars robot_tests/conda.yaml + Step build/rcc ht vars --controller citests robot_tests/conda.yaml Must Exist %{ROBOCORP_HOME}/bin/ Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot new file mode 100644 index 00000000..caff42e0 --- /dev/null +++ b/robot_tests/templates.robot @@ -0,0 +1,70 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot + +*** Test cases *** + +Goal: Initialize new standard robot. + Step build/rcc robot init --controller citests -t standard -d tmp/standardi -f + Use STDERR + Must Have OK. + +Goal: Standard robot has correct hash. + Step build/rcc holotree hash --silent --controller citests tmp/standardi/conda.yaml + Must Have 55aacd3b136421fd + +Goal: Running standard robot is succesful. + Step build/rcc task run --space templates --controller citests --robot tmp/standardi/robot.yaml + Use STDERR + Must Have OK. + +Goal: Initialize new python robot. + Step build/rcc robot init --controller citests -t python -d tmp/pythoni -f + Use STDERR + Must Have OK. + +Goal: Python robot has correct hash. + Step build/rcc holotree hash --silent --controller citests tmp/pythoni/conda.yaml + Must Have 55aacd3b136421fd + +Goal: Running python robot is succesful. + Step build/rcc task run --space templates --controller citests --robot tmp/pythoni/robot.yaml + Use STDERR + Must Have OK. + +Goal: Initialize new extended robot. + Step build/rcc robot init --controller citests -t extended -d tmp/extendedi -f + Use STDERR + Must Have OK. + +Goal: Extended robot has correct hash. + Step build/rcc holotree hash --silent --controller citests tmp/extendedi/conda.yaml + Must Have 55aacd3b136421fd + +Goal: Running extended robot is succesful. (Run All Tasks) + Step build/rcc task run --space templates --task "Run All Tasks" --controller citests --robot tmp/extendedi/robot.yaml + Use STDERR + Must Have OK. + +Goal: Running extended robot is succesful. (Run Example Task) + Step build/rcc task run --space templates --task "Run Example Task" --controller citests --robot tmp/extendedi/robot.yaml + Use STDERR + Must Have OK. + +Goal: Correct holotree spaces were created. + Step build/rcc holotree list + Use STDERR + Must Have rcc.citests + Must Have templates + Wont Have rcc.user + +Goal: Can get plan for used environment. + Step build/rcc holotree plan 4e67cd8d4_c6880905 + Must Have micromamba plan + Must Have pip plan + Must Have post install plan + Must Have activation plan + Must Have installation plan complete + Use STDERR + Must Have OK. From c61d2c0c75a604543cbddb14efdefea427e076d0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 17 Sep 2021 12:17:01 +0300 Subject: [PATCH 191/516] RCC-199: hololib corruption bug (v11.1.1) - bugfix: using rename in hololib file copy to make it more transactional - progress indicator now has elapsed time since previous progress entry - experimental upgrade to use go 1.17 on Github Actions --- .github/workflows/rcc.yaml | 4 +- common/logger.go | 5 +- common/variables.go | 2 + common/version.go | 2 +- docs/changelog.md | 6 ++ htfs/functions.go | 120 +++++++++++-------------------- pathlib/copyfile.go | 28 -------- pathlib/lock_unix.go | 1 + pathlib/lock_windows.go | 1 + pretty/setup_windows.go | 1 + robot_tests/export_holozip.robot | 2 +- robot_tests/fullrun.robot | 16 ++--- 12 files changed, 69 insertions(+), 119 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index df19057a..d0fd6de7 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: '1.16.x' + go-version: '1.17.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: '1.16.x' + go-version: '1.17.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' diff --git a/common/logger.go b/common/logger.go index 0852a72c..c6c2c01e 100644 --- a/common/logger.go +++ b/common/logger.go @@ -98,7 +98,10 @@ func WaitLogs() { } func Progress(step int, form string, details ...interface{}) { + previous := ProgressMark + ProgressMark = time.Now() + delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() message := fmt.Sprintf(form, details...) - Log("#### Progress: %d/12 %s %s", step, Version, message) + Log("#### Progress: %02d/12 %s %8.3fs %s", step, Version, delta, message) Timeline("%d/12 %s", step, message) } diff --git a/common/variables.go b/common/variables.go index 46101811..4af99187 100644 --- a/common/variables.go +++ b/common/variables.go @@ -29,12 +29,14 @@ var ( EnvironmentHash string SemanticTag string When int64 + ProgressMark time.Time Clock *stopwatch ) func init() { Clock = &stopwatch{"Clock", time.Now()} When = Clock.started.Unix() + ProgressMark = time.Now() } func RobocorpHome() string { diff --git a/common/version.go b/common/version.go index 8a663126..98cfb27f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.0` + Version = `v11.1.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 99e0a825..13f3ee17 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.1.1 (date: 17.9.2021) + +- bugfix: using rename in hololib file copy to make it more transactional +- progress indicator now has elapsed time since previous progress entry +- experimental upgrade to use go 1.17 on Github Actions + ## v11.1.0 (date: 16.9.2021) - BREAKING CHANGES, but now this may be considered stable(ish) diff --git a/htfs/functions.go b/htfs/functions.go index 5ac0fc27..a84e7a63 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -173,109 +173,73 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { return scheduler } +func onErrPanicClose(err error, sink io.Closer) { + if err != nil { + if sink != nil { + sink.Close() + } + panic(err) + } +} + func LiftFile(sourcename, sinkname string) anywork.Work { return func() { source, err := os.Open(sourcename) - if err != nil { - panic(err) - } + onErrPanicClose(err, nil) + defer source.Close() - sink, err := os.Create(sinkname) - if err != nil { - panic(err) - } + partname := fmt.Sprintf("%s.part%s", sinkname, <-common.Identities) + defer os.Remove(partname) + sink, err := os.Create(partname) + onErrPanicClose(err, nil) + defer sink.Close() writer, err := gzip.NewWriterLevel(sink, gzip.BestSpeed) - if err != nil { - panic(err) - } + onErrPanicClose(err, sink) + _, err = io.Copy(writer, source) - if err != nil { - panic(err) - } + onErrPanicClose(err, sink) + err = writer.Close() - if err != nil { - panic(err) - } - } -} + onErrPanicClose(err, sink) -func LiftFlatFile(sourcename, sinkname string) anywork.Work { - return func() { - source, err := os.Open(sourcename) - if err != nil { - panic(err) - } - defer source.Close() - sink, err := os.Create(sinkname) - if err != nil { - panic(err) - } - defer sink.Close() - _, err = io.Copy(sink, source) - if err != nil { - panic(err) - } + err = sink.Close() + onErrPanicClose(err, nil) + + err = os.Rename(partname, sinkname) + onErrPanicClose(err, nil) } } func DropFile(library Library, digest, sinkname string, details *File, rewrite []byte) anywork.Work { return func() { reader, closer, err := library.Open(digest) - if err != nil { - panic(err) - } + onErrPanicClose(err, nil) + defer closer() - sink, err := os.Create(sinkname) - if err != nil { - panic(err) - } - defer sink.Close() + partname := fmt.Sprintf("%s.part%s", sinkname, <-common.Identities) + defer os.Remove(partname) + sink, err := os.Create(partname) + onErrPanicClose(err, nil) + _, err = io.Copy(sink, reader) - if err != nil { - panic(err) - } - for _, position := range details.Rewrite { - _, err = sink.Seek(position, 0) - if err != nil { - panic(fmt.Sprintf("%v %d", err, position)) - } - _, err = sink.Write(rewrite) - if err != nil { - panic(err) - } - } - os.Chmod(sinkname, details.Mode) - os.Chtimes(sinkname, motherTime, motherTime) - } -} + onErrPanicClose(err, sink) -func DropFlatFile(sourcename, sinkname string, details *File, rewrite []byte) anywork.Work { - return func() { - source, err := os.Open(sourcename) - if err != nil { - panic(err) - } - defer source.Close() - sink, err := os.Create(sinkname) - if err != nil { - panic(err) - } - defer sink.Close() - _, err = io.Copy(sink, source) - if err != nil { - panic(err) - } for _, position := range details.Rewrite { _, err = sink.Seek(position, 0) if err != nil { + sink.Close() panic(fmt.Sprintf("%v %d", err, position)) } _, err = sink.Write(rewrite) - if err != nil { - panic(err) - } + onErrPanicClose(err, sink) } + err = sink.Close() + onErrPanicClose(err, nil) + + err = os.Rename(partname, sinkname) + onErrPanicClose(err, nil) + os.Chmod(sinkname, details.Mode) os.Chtimes(sinkname, motherTime, motherTime) } diff --git a/pathlib/copyfile.go b/pathlib/copyfile.go index fc8290ba..993ede6c 100644 --- a/pathlib/copyfile.go +++ b/pathlib/copyfile.go @@ -1,8 +1,6 @@ package pathlib import ( - "compress/flate" - "compress/gzip" "io" "os" "path/filepath" @@ -12,34 +10,8 @@ import ( type copyfunc func(io.Writer, io.Reader) (int64, error) -func archiver(target io.Writer, source io.Reader) (int64, error) { - wrapper, err := gzip.NewWriterLevel(target, flate.BestSpeed) - if err != nil { - return 0, err - } - defer wrapper.Close() - return io.Copy(wrapper, source) -} - -func restorer(target io.Writer, source io.Reader) (int64, error) { - wrapper, err := gzip.NewReader(source) - if err != nil { - return 0, err - } - defer wrapper.Close() - return io.Copy(target, wrapper) -} - type Copier func(string, string, bool) error -func ArchiveFile(source, target string, overwrite bool) error { - return copyFile(source, target, overwrite, archiver) -} - -func RestoreFile(source, target string, overwrite bool) error { - return copyFile(source, target, overwrite, restorer) -} - func CopyFile(source, target string, overwrite bool) error { mark, err := Modtime(source) if err != nil { diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 9d289156..c89dab84 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -1,3 +1,4 @@ +//go:build darwin || linux || !windows // +build darwin linux !windows package pathlib diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 12c88096..cdad24f8 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package pathlib diff --git a/pretty/setup_windows.go b/pretty/setup_windows.go index 449dc3f2..5ce5d68b 100644 --- a/pretty/setup_windows.go +++ b/pretty/setup_windows.go @@ -1,3 +1,4 @@ +//go:build windows || !darwin || !linux // +build windows !darwin !linux package pretty diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index a95bdf18..7635c6b0 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -32,7 +32,7 @@ Goal: Create environment for standalone robot Must Have 4e67cd8d4_fcb4b859 Use STDERR Must Have Downloading micromamba - Must Have Progress: 1/12 + Must Have Progress: 01/12 Must Have Progress: 12/12 Goal: Must have author space visible diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 9be7ce63..6ce71829 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -77,9 +77,9 @@ Goal: Run task in place in debug mode and with timeline. Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR - Must Have Progress: 1/12 - Must Have Progress: 2/12 - Must Have Progress: 3/12 + Must Have Progress: 01/12 + Must Have Progress: 02/12 + Must Have Progress: 03/12 Must Have Progress: 12/12 Must Have rpaframework Must Have PID # @@ -111,11 +111,11 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 1/12 - Wont Have Progress: 3/12 - Wont Have Progress: 5/12 - Wont Have Progress: 7/12 - Wont Have Progress: 9/12 + Must Have Progress: 01/12 + Wont Have Progress: 03/12 + Wont Have Progress: 05/12 + Wont Have Progress: 07/12 + Wont Have Progress: 09/12 Must Have Progress: 11/12 Must Have Progress: 12/12 Must Have OK. From c29ba7c34d9ed9db56c1ad62354da27a98df4550 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 20 Sep 2021 11:01:12 +0300 Subject: [PATCH 192/516] RCC-199: hololib corruption bug (v11.1.2) - bugfix: removing duplicate file copy on holotree recording - removed "new live" phrase from debug printouts - made robot tests to check holotree integrity in some selected points --- common/version.go | 2 +- conda/workflows.go | 16 ++++++++-------- docs/changelog.md | 6 ++++++ htfs/functions.go | 31 ++++++++++++++++++------------- robot_tests/fullrun.robot | 4 ++++ robot_tests/templates.robot | 5 +++++ 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/common/version.go b/common/version.go index 98cfb27f..b4c11622 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.1` + Version = `v11.1.2` ) diff --git a/conda/workflows.go b/conda/workflows.go index 38738551..4e3858fd 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -89,18 +89,18 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall return false, fmt.Errorf("Could not get micromamba installed.") } targetFolder := common.StageFolder - common.Debug("=== new live --- pre cleanup phase ===") + common.Debug("=== pre cleanup phase ===") common.Timeline("pre cleanup phase.") err := renameRemove(targetFolder) if err != nil { return false, err } - common.Debug("=== new live --- first try phase ===") + common.Debug("=== first try phase ===") common.Timeline("first try.") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) - common.Debug("=== new live --- second try phase ===") + common.Debug("=== second try phase ===") common.Timeline("second try.") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") @@ -143,7 +143,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") observer := make(InstallObserver) - common.Debug("=== new live --- micromamba create phase ===") + common.Debug("=== micromamba create phase ===") fmt.Fprintf(planWriter, "\n--- micromamba plan @%ss ---\n\n", stopwatch) tee := io.MultiWriter(observer, planWriter) code, err := shell.New(CondaEnvironment(), ".", mambaCommand.CLI()...).Tracked(tee, false) @@ -169,7 +169,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand.Option("--index-url", settings.Global.PypiURL()) pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") - common.Debug("=== new live --- pip install phase ===") + common.Debug("=== pip install phase ===") code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) @@ -183,7 +183,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { common.Progress(7, "Post install scripts phase started.") - common.Debug("=== new live --- post install phase ===") + common.Debug("=== post install phase ===") for _, script := range postInstall { scriptCommand, err := shlex.Split(script) if err != nil { @@ -203,7 +203,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Progress(7, "Post install scripts phase skipped -- no scripts.") } common.Progress(8, "Activate environment started phase.") - common.Debug("=== new live --- activate phase ===") + common.Debug("=== activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) err = Activate(planWriter, targetFolder) if err != nil { @@ -222,7 +222,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Progress(9, "Update installation plan.") finalplan := filepath.Join(targetFolder, "rcc_plan.log") os.Rename(planfile, finalplan) - common.Debug("=== new live --- finalize phase ===") + common.Debug("=== finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") err = ioutil.WriteFile(markerFile, []byte(yaml), 0o644) diff --git a/docs/changelog.md b/docs/changelog.md index 13f3ee17..d78e93da 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.1.2 (date: 20.9.2021) + +- bugfix: removing duplicate file copy on holotree recording +- removed "new live" phrase from debug printouts +- made robot tests to check holotree integrity in some selected points + ## v11.1.1 (date: 17.9.2021) - bugfix: using rename in hololib file copy to make it more transactional diff --git a/htfs/functions.go b/htfs/functions.go index a84e7a63..b4f81ed9 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -150,15 +150,22 @@ func MakeBranches(path string, it *Dir) error { func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { var scheduler Treetop + seen := make(map[string]bool) scheduler = func(path string, it *Dir) error { for name, subdir := range it.Dirs { scheduler(filepath.Join(path, name), subdir) } for name, file := range it.Files { + if seen[file.Digest] { + common.Trace("LiftFile %s %q already scheduled.", file.Digest, name) + continue + } + seen[file.Digest] = true directory := library.Location(file.Digest) - if !pathlib.IsDir(directory) { + if !seen[directory] && !pathlib.IsDir(directory) { os.MkdirAll(directory, 0o755) } + seen[directory] = true sinkpath := filepath.Join(directory, file.Digest) ok := pathlib.IsFile(sinkpath) stats.Dirty(!ok) @@ -200,14 +207,13 @@ func LiftFile(sourcename, sinkname string) anywork.Work { _, err = io.Copy(writer, source) onErrPanicClose(err, sink) - err = writer.Close() - onErrPanicClose(err, sink) + onErrPanicClose(writer.Close(), sink) - err = sink.Close() - onErrPanicClose(err, nil) + onErrPanicClose(sink.Close(), nil) - err = os.Rename(partname, sinkname) - onErrPanicClose(err, nil) + runtime.Gosched() + + onErrPanicClose(os.Rename(partname, sinkname), nil) } } @@ -234,14 +240,13 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ _, err = sink.Write(rewrite) onErrPanicClose(err, sink) } - err = sink.Close() - onErrPanicClose(err, nil) - err = os.Rename(partname, sinkname) - onErrPanicClose(err, nil) + onErrPanicClose(sink.Close(), nil) + + onErrPanicClose(os.Rename(partname, sinkname), nil) - os.Chmod(sinkname, details.Mode) - os.Chtimes(sinkname, motherTime, motherTime) + onErrPanicClose(os.Chmod(sinkname, details.Mode), nil) + onErrPanicClose(os.Chtimes(sinkname, motherTime, motherTime), nil) } } diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6ce71829..06652f7d 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -105,6 +105,7 @@ Goal: Run task in place in debug mode and with timeline. Must Exist tmp/fluffy/output/environment_*_freeze.yaml Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ + Step build/rcc holotree check --controller citests Goal: Run task in clean temporary directory. Step build/rcc task testrun --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml @@ -157,6 +158,7 @@ Goal: See variables from specific environment without robot.yaml knowledge Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= Must Have 54399f4561ae95af + Step build/rcc holotree check --controller citests Goal: See variables from specific environment with robot.yaml but without task Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml @@ -178,6 +180,7 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have PYTHONPATH= Must Have ROBOT_ROOT= Must Have ROBOT_ARTIFACTS= + Step build/rcc holotree check --controller citests Goal: See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc holotree variables --controller citests --json conda/testdata/conda.yaml @@ -208,6 +211,7 @@ Goal: See variables from specific environment with robot.yaml knowledge Wont Have RC_API_SECRET_TOKEN= Wont Have RC_API_WORKITEM_TOKEN= Wont Have RC_WORKSPACE_ID= + Step build/rcc holotree check --controller citests Goal: See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc holotree variables --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index caff42e0..bb31a09a 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -68,3 +68,8 @@ Goal: Can get plan for used environment. Must Have installation plan complete Use STDERR Must Have OK. + +Goal: Holotree is still correct. + Step build/rcc holotree check --controller citests + Use STDERR + Must Have OK. From e1343f0dfcf18ce2d2373ead07d41ffc684ab3b1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 21 Sep 2021 13:52:01 +0300 Subject: [PATCH 193/516] RCC-199: hololib corruption bug (v11.1.3) - bugfix: changing performance thru auto-scaling workers based on number of CPUs (minus one, but at least 4 workers) --- anywork/worker.go | 28 +++++++++++++++++++++-- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 8 +++---- htfs/functions.go | 57 ++++++++++++++++------------------------------- 5 files changed, 54 insertions(+), 46 deletions(-) diff --git a/anywork/worker.go b/anywork/worker.go index 6ef9fabe..cfd0f236 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -2,7 +2,9 @@ package anywork import ( "fmt" + "io" "os" + "runtime" "sync" ) @@ -62,11 +64,22 @@ func init() { failpipe = make(Failures) errcount = make(Counters) headcount = 0 - Scale(16) + AutoScale() go watcher(failpipe, errcount) } -func Scale(limit uint64) { +func Scale() uint64 { + return headcount +} + +func AutoScale() { + limit := uint64(runtime.NumCPU() - 1) + if limit > 96 { + limit = 96 + } + if limit < 4 { + limit = 4 + } for headcount < limit { go member(headcount) headcount += 1 @@ -89,6 +102,17 @@ func Sync() error { return nil } +func OnErrPanicCloseAll(err error, closers ...io.Closer) { + if err != nil { + for _, closer := range closers { + if closer != nil { + closer.Close() + } + } + panic(err) + } +} + func Done() error { close(pipeline) return Sync() diff --git a/common/version.go b/common/version.go index b4c11622..4c9a6ab7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.2` + Version = `v11.1.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index d78e93da..15998eb0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.1.3 (date: 21.9.2021) + +- bugfix: changing performance thru auto-scaling workers based on number + of CPUs (minus one, but at least 4 workers) + ## v11.1.2 (date: 20.9.2021) - bugfix: removing duplicate file copy on holotree recording diff --git a/htfs/commands.go b/htfs/commands.go index 3e4667f7..17c5205c 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -24,7 +24,7 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) - defer common.Progress(12, "Fresh holotree done.") + defer common.Progress(12, "Fresh holotree done [with %d workers].", anywork.Scale()) common.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) callback := pathlib.LockWaitMessage("Serialized environment creation") @@ -40,8 +40,6 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er common.EnvironmentHash = BlueprintHash(holotreeBlueprint) common.Progress(2, "Holotree blueprint is %q.", common.EnvironmentHash) - anywork.Scale(100) - tree, err := New() fail.On(err != nil, "%s", err) @@ -60,7 +58,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er library = tree } - common.Progress(11, "Restore space from library.") + common.Progress(11, "Restore space from library [with %d workers].", anywork.Scale()) path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) return path, nil @@ -114,7 +112,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e err = conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) - common.Progress(10, "Record holotree stage to hololib.") + common.Progress(10, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } diff --git a/htfs/functions.go b/htfs/functions.go index b4f81ed9..8c0fff38 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -180,56 +180,47 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { return scheduler } -func onErrPanicClose(err error, sink io.Closer) { - if err != nil { - if sink != nil { - sink.Close() - } - panic(err) - } -} - func LiftFile(sourcename, sinkname string) anywork.Work { return func() { source, err := os.Open(sourcename) - onErrPanicClose(err, nil) + anywork.OnErrPanicCloseAll(err) defer source.Close() partname := fmt.Sprintf("%s.part%s", sinkname, <-common.Identities) defer os.Remove(partname) sink, err := os.Create(partname) - onErrPanicClose(err, nil) + anywork.OnErrPanicCloseAll(err) defer sink.Close() writer, err := gzip.NewWriterLevel(sink, gzip.BestSpeed) - onErrPanicClose(err, sink) + anywork.OnErrPanicCloseAll(err, sink) _, err = io.Copy(writer, source) - onErrPanicClose(err, sink) + anywork.OnErrPanicCloseAll(err, sink) - onErrPanicClose(writer.Close(), sink) + anywork.OnErrPanicCloseAll(writer.Close(), sink) - onErrPanicClose(sink.Close(), nil) + anywork.OnErrPanicCloseAll(sink.Close()) runtime.Gosched() - onErrPanicClose(os.Rename(partname, sinkname), nil) + anywork.OnErrPanicCloseAll(os.Rename(partname, sinkname)) } } func DropFile(library Library, digest, sinkname string, details *File, rewrite []byte) anywork.Work { return func() { reader, closer, err := library.Open(digest) - onErrPanicClose(err, nil) + anywork.OnErrPanicCloseAll(err) defer closer() partname := fmt.Sprintf("%s.part%s", sinkname, <-common.Identities) defer os.Remove(partname) sink, err := os.Create(partname) - onErrPanicClose(err, nil) + anywork.OnErrPanicCloseAll(err) _, err = io.Copy(sink, reader) - onErrPanicClose(err, sink) + anywork.OnErrPanicCloseAll(err, sink) for _, position := range details.Rewrite { _, err = sink.Seek(position, 0) @@ -238,33 +229,27 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ panic(fmt.Sprintf("%v %d", err, position)) } _, err = sink.Write(rewrite) - onErrPanicClose(err, sink) + anywork.OnErrPanicCloseAll(err, sink) } - onErrPanicClose(sink.Close(), nil) + anywork.OnErrPanicCloseAll(sink.Close()) - onErrPanicClose(os.Rename(partname, sinkname), nil) + anywork.OnErrPanicCloseAll(os.Rename(partname, sinkname)) - onErrPanicClose(os.Chmod(sinkname, details.Mode), nil) - onErrPanicClose(os.Chtimes(sinkname, motherTime, motherTime), nil) + anywork.OnErrPanicCloseAll(os.Chmod(sinkname, details.Mode)) + anywork.OnErrPanicCloseAll(os.Chtimes(sinkname, motherTime, motherTime)) } } func RemoveFile(filename string) anywork.Work { return func() { - err := os.Remove(filename) - if err != nil { - panic(err) - } + anywork.OnErrPanicCloseAll(os.Remove(filename)) } } func RemoveDirectory(dirname string) anywork.Work { return func() { - err := os.RemoveAll(dirname) - if err != nil { - panic(err) - } + anywork.OnErrPanicCloseAll(os.RemoveAll(dirname)) } } @@ -272,16 +257,12 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat return func(path string, it *Dir) anywork.Work { return func() { content, err := os.ReadDir(path) - if err != nil { - panic(err) - } + anywork.OnErrPanicCloseAll(err) files := make(map[string]bool) for _, part := range content { directpath := filepath.Join(path, part.Name()) info, err := os.Stat(directpath) - if err != nil { - panic(err) - } + anywork.OnErrPanicCloseAll(err) if info.IsDir() { _, ok := it.Dirs[part.Name()] if !ok { From 4526add53d55c255d8e1b1af04699e1220c0eeb2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 23 Sep 2021 12:04:41 +0300 Subject: [PATCH 194/516] RCC-199: hololib corruption bug (v11.1.4) - bugfix: adding concurrencty to catalog check - performance profiling revealed bottleneck, where ensuring directory exist was called too often, so now base directories are ensured only once per rcc invocation - adding more structure to timeline printout by indentation of blocks --- cmd/holotreeVariables.go | 5 ++++ cmd/rcc/main.go | 3 ++- cmd/root.go | 3 +++ common/timeline.go | 53 ++++++++++++++++++++++++++++++++-------- common/variables.go | 41 +++++++++++++++++++------------ common/version.go | 2 +- docs/changelog.md | 8 ++++++ htfs/directory.go | 16 ++++++------ htfs/functions.go | 20 +++++++++------ htfs/library.go | 23 ++++++++--------- htfs/ziplibrary.go | 12 ++++----- 11 files changed, 126 insertions(+), 60 deletions(-) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 3daa02cf..4fa20f1b 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -64,6 +64,8 @@ func asExportedText(items []string) { func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, force bool) []string { var extra []string var data operations.Token + common.TimelineBegin("environment expansion start") + defer common.TimelineEnd() config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) @@ -80,18 +82,21 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { + common.Timeline("load robot environment") developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) if err == nil { extra = developmentEnvironment.AsEnvironment() } } + common.Timeline("load robot environment") env := conda.EnvironmentExtensionFor(path) if config != nil { env = config.ExecutionEnvironment(path, extra, false) } if Has(workspace) { + common.Timeline("get run robot claims") claims := operations.RunRobotClaims(validity*60, workspace) data, err = operations.AuthorizeClaims(AccountName(), claims) pretty.Guard(err == nil, 9, "Failed to get cloud data, reason: %v", err) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 66cc68c3..ee3b6f50 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -60,6 +60,7 @@ func ExitProtection() { } func startTempRecycling() { + defer common.Timeline("temp recycling done") pattern := filepath.Join(common.RobocorpTempRoot(), "*", "recycle.now") found, err := filepath.Glob(pattern) if err != nil { @@ -87,7 +88,7 @@ func markTempForRecycling() { } func main() { - common.Timeline("Start.") + common.TimelineBegin("Start.") defer common.EndOfTimeline() defer ExitProtection() diff --git a/cmd/root.go b/cmd/root.go index 1362bfae..cc1ced91 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -76,9 +76,11 @@ func Origin() string { func Execute() { defer func() { if profiling != nil { + common.Timeline("closing profiling started") pprof.StopCPUProfile() profiling.Sync() profiling.Close() + common.TimelineEnd() } }() @@ -109,6 +111,7 @@ func init() { func initConfig() { if profilefile != "" { + common.TimelineBegin("profiling run started") sink, err := os.Create(profilefile) pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", profilefile, err) err = pprof.StartCPUProfile(sink) diff --git a/common/timeline.go b/common/timeline.go index 810e0d55..a4810a57 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -2,37 +2,57 @@ package common import ( "fmt" + "strings" ) var ( TimelineEnabled bool pipe chan string + indent chan bool done chan bool ) type timevent struct { - when Duration - what string + level int + when Duration + what string } -func timeliner(events chan string, done chan bool) { +func timeliner(events chan string, indent, done chan bool) { history := make([]*timevent, 0, 100) + level := 0 +loop: for { - event, ok := <-events - if !ok { - break + select { + case event, ok := <-events: + if !ok { + break loop + } + history = append(history, &timevent{level, Clock.Elapsed(), event}) + case deeper, ok := <-indent: + if !ok { + break loop + } + if deeper { + level += 1 + } else { + level -= 1 + } + if level < 0 { + level = 0 + } } - history = append(history, &timevent{Clock.Elapsed(), event}) } death := Clock.Elapsed() if TimelineEnabled && death.Milliseconds() > 0 { - history = append(history, &timevent{death, "Now."}) + history = append(history, &timevent{0, death, "Now."}) Log("---- rcc timeline ----") Log(" # percent seconds event") for at, event := range history { permille := event.when * 1000 / death percent := float64(permille) / 10.0 - Log("%2d: %5.1f%% %7s %s", at+1, percent, event.when, event.what) + indent := strings.Repeat("| ", event.level) + Log("%2d: %5.1f%% %7s %s%s", at+1, percent, event.when, indent, event.what) } Log("---- rcc timeline ----") } @@ -41,8 +61,9 @@ func timeliner(events chan string, done chan bool) { func init() { pipe = make(chan string) + indent = make(chan bool) done = make(chan bool) - go timeliner(pipe, done) + go timeliner(pipe, indent, done) } func IgnoreAllPanics() { @@ -54,7 +75,19 @@ func Timeline(form string, details ...interface{}) { pipe <- fmt.Sprintf(form, details...) } +func TimelineBegin(form string, details ...interface{}) { + Timeline(form, details...) + indent <- true +} + +func TimelineEnd() { + indent <- false + Timeline("`") +} + func EndOfTimeline() { + TimelineEnd() close(pipe) + close(indent) <-done } diff --git a/common/variables.go b/common/variables.go index 4af99187..a6e1ad9a 100644 --- a/common/variables.go +++ b/common/variables.go @@ -37,14 +37,24 @@ func init() { Clock = &stopwatch{"Clock", time.Now()} When = Clock.started.Unix() ProgressMark = time.Now() + + ensureDirectory(TemplateLocation()) + ensureDirectory(BinLocation()) + ensureDirectory(HolotreeLocation()) + ensureDirectory(HololibCatalogLocation()) + ensureDirectory(HololibLibraryLocation()) + ensureDirectory(PipCache()) + ensureDirectory(WheelCache()) + ensureDirectory(RobotCache()) + ensureDirectory(MambaPackages()) } func RobocorpHome() string { home := os.Getenv(ROBOCORP_HOME_VARIABLE) if len(home) > 0 { - return ensureDirectory(ExpandPath(home)) + return ExpandPath(home) } - return ensureDirectory(ExpandPath(defaultRobocorpLocation)) + return ExpandPath(defaultRobocorpLocation) } func RobocorpLock() string { @@ -59,11 +69,6 @@ func OverrideSystemRequirements() bool { return len(os.Getenv(ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS)) > 0 } -func ensureDirectory(name string) string { - os.MkdirAll(name, 0o750) - return name -} - func BinRcc() string { self, err := os.Executable() if err != nil { @@ -77,7 +82,7 @@ func EventJournal() string { } func TemplateLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "templates")) + return filepath.Join(RobocorpHome(), "templates") } func RobocorpTempRoot() string { @@ -85,19 +90,19 @@ func RobocorpTempRoot() string { } func BinLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "bin")) + return filepath.Join(RobocorpHome(), "bin") } func HololibLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "hololib")) + return filepath.Join(RobocorpHome(), "hololib") } func HololibCatalogLocation() string { - return ensureDirectory(filepath.Join(HololibLocation(), "catalog")) + return filepath.Join(HololibLocation(), "catalog") } func HololibLibraryLocation() string { - return ensureDirectory(filepath.Join(HololibLocation(), "library")) + return filepath.Join(HololibLocation(), "library") } func HolotreeLock() string { @@ -105,7 +110,7 @@ func HolotreeLock() string { } func HolotreeLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "holotree")) + return filepath.Join(RobocorpHome(), "holotree") } func UsesHolotree() bool { @@ -113,15 +118,15 @@ func UsesHolotree() bool { } func PipCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "pipcache")) + return filepath.Join(RobocorpHome(), "pipcache") } func WheelCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "wheels")) + return filepath.Join(RobocorpHome(), "wheels") } func RobotCache() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "robots")) + return filepath.Join(RobocorpHome(), "robots") } func MambaPackages() string { @@ -157,3 +162,7 @@ func UserAgent() string { func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } + +func ensureDirectory(name string) { + Error("mkdir", os.MkdirAll(name, 0o750)) +} diff --git a/common/version.go b/common/version.go index 4c9a6ab7..196b7ab6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.3` + Version = `v11.1.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 15998eb0..d104e122 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.1.4 (date: 23.9.2021) + +- bugfix: adding concurrencty to catalog check +- performance profiling revealed bottleneck, where ensuring directory exist + was called too often, so now base directories are ensured only once per + rcc invocation +- adding more structure to timeline printout by indentation of blocks + ## v11.1.3 (date: 21.9.2021) - bugfix: changing performance thru auto-scaling workers based on number diff --git a/htfs/directory.go b/htfs/directory.go index 67d2547d..026d705f 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -98,8 +98,8 @@ func (it *Root) Lift() error { } func (it *Root) Treetop(task Treetop) error { - common.Timeline("holotree treetop sync start") - defer common.Timeline("holotree treetop sync done") + common.TimelineBegin("holotree treetop sync start") + defer common.TimelineEnd() err := task(it.Path, it.Tree) if err != nil { return err @@ -108,15 +108,15 @@ func (it *Root) Treetop(task Treetop) error { } func (it *Root) AllDirs(task Dirtask) error { - common.Timeline("holotree dirs sync start") - defer common.Timeline("holotree dirs sync done") + common.TimelineBegin("holotree dirs sync start") + defer common.TimelineEnd() it.Tree.AllDirs(it.Path, task) return anywork.Sync() } func (it *Root) AllFiles(task Filetask) error { - common.Timeline("holotree files sync start") - defer common.Timeline("holotree files sync done") + common.TimelineBegin("holotree files sync start") + defer common.TimelineEnd() it.Tree.AllFiles(it.Path, task) return anywork.Sync() } @@ -154,8 +154,8 @@ func (it *Root) ReadFrom(source io.Reader) error { } func (it *Root) LoadFrom(filename string) error { - common.Timeline("holotree load %q", filename) - defer common.Timeline("holotree load done") + common.TimelineBegin("holotree load %q", filename) + defer common.TimelineEnd() source, err := os.Open(filename) if err != nil { return err diff --git a/htfs/functions.go b/htfs/functions.go index 8c0fff38..216b1f4d 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -17,15 +17,21 @@ import ( "github.com/robocorp/rcc/trollhash" ) +func JustFileExistCheck(library MutableLibrary, path, name, digest string) anywork.Work { + return func() { + location := library.ExactLocation(digest) + if !pathlib.IsFile(location) { + fullpath := filepath.Join(path, name) + panic(fmt.Errorf("Content for %q [%s] is missing!", fullpath, digest)) + } + } +} + func CatalogCheck(library MutableLibrary, fs *Root) Treetop { var tool Treetop tool = func(path string, it *Dir) error { for name, file := range it.Files { - location := library.ExactLocation(file.Digest) - if !pathlib.IsFile(location) { - fullpath := filepath.Join(path, name) - return fmt.Errorf("Content for %q [%s] is missing!", fullpath, file.Digest) - } + anywork.Backlog(JustFileExistCheck(library, path, name, file.Digest)) } for name, subdir := range it.Dirs { err := tool(filepath.Join(path, name), subdir) @@ -365,8 +371,8 @@ func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { } func LoadCatalogs() ([]string, []*Root) { - common.Timeline("catalog load start") - defer common.Timeline("catalog load done") + common.TimelineBegin("catalog load start") + defer common.TimelineEnd() catalogs := Catalogs() roots := make([]*Root, len(catalogs)) for at, catalog := range catalogs { diff --git a/htfs/library.go b/htfs/library.go index 22c14131..66b766c1 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -122,8 +122,8 @@ func (it zipseen) Add(fullpath, relativepath string) (err error) { func (it *hololib) Export(catalogs []string, archive string) (err error) { defer fail.Around(&err) - common.Timeline("holotree export start") - defer common.Timeline("holotree export done") + common.TimelineBegin("holotree export start") + defer common.TimelineEnd() handle, err := os.Create(archive) fail.On(err != nil, "Could not create archive %q.", archive) @@ -216,9 +216,9 @@ func (it *hololib) queryBlueprint(key string) bool { common.Debug("Catalog load failed, reason: %v", err) return false } - common.Timeline("holotree content check start") + common.TimelineBegin("holotree content check start") err = shadow.Treetop(CatalogCheck(it, shadow)) - common.Timeline("holotree content check done") + common.TimelineEnd() if err != nil { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.holotree.catalog.failure", common.Version) common.Debug("Catalog check failed, reason: %v", err) @@ -273,7 +273,8 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) catalog := it.CatalogPath(key) - common.Timeline("holotree restore start %s", key) + common.TimelineBegin("holotree space restore start [%s]", key) + defer common.TimelineEnd() name := ControllerSpaceName(client, tag) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) @@ -292,21 +293,21 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er err = shadow.LoadFrom(metafile) } if err == nil { - common.Timeline("holotree digest start") + common.TimelineBegin("holotree digest start") shadow.Treetop(DigestRecorder(currentstate)) - common.Timeline("holotree digest done") + common.TimelineEnd() } err = fs.Relocate(targetdir) fail.On(err != nil, "Failed to relocate %s -> %v", targetdir, err) - common.Timeline("holotree make branches start") + common.TimelineBegin("holotree make branches start") err = fs.Treetop(MakeBranches) - common.Timeline("holotree make branches done") + common.TimelineEnd() fail.On(err != nil, "Failed to make branches -> %v", err) score := &stats{} - common.Timeline("holotree restore start") + common.TimelineBegin("holotree restore start") err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) fail.On(err != nil, "Failed to restore directories -> %v", err) - common.Timeline("holotree restore done") + common.TimelineEnd() defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) fs.Controller = string(client) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 6d443133..65d0bab3 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -101,21 +101,21 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err err = shadow.LoadFrom(metafile) } if err == nil { - common.Timeline("holotree digest start (zip)") + common.TimelineBegin("holotree digest start (zip)") shadow.Treetop(DigestRecorder(currentstate)) - common.Timeline("holotree digest done (zip)") + common.TimelineEnd() } err = fs.Relocate(targetdir) fail.On(err != nil, "Failed to relocate %q -> %v", targetdir, err) - common.Timeline("holotree make branches start (zip)") + common.TimelineBegin("holotree make branches start (zip)") err = fs.Treetop(MakeBranches) - common.Timeline("holotree make branches done (zip)") + common.TimelineEnd() fail.On(err != nil, "Failed to make branches %q -> %v", targetdir, err) score := &stats{} - common.Timeline("holotree restore start (zip)") + common.TimelineBegin("holotree restore start (zip)") err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) fail.On(err != nil, "Failed to restore directory %q -> %v", targetdir, err) - common.Timeline("holotree restore done (zip)") + common.TimelineEnd() defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) fs.Controller = string(client) From 685d2ace46f77f8ddadb00786d86a91ac1f8b50b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 24 Sep 2021 16:15:10 +0300 Subject: [PATCH 195/516] RCC-199: hololib corruption bug (v11.1.5) - bugfix: performance profiling revealed bottleneck in windows, where calling stat is expensive, so here is try to limit using it uneccessarily --- common/version.go | 2 +- docs/changelog.md | 5 +++++ pathlib/functions.go | 8 +++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 196b7ab6..f895ee9a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.4` + Version = `v11.1.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index d104e122..0e47f70b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.1.5 (date: 24.9.2021) + +- bugfix: performance profiling revealed bottleneck in windows, where calling + stat is expensive, so here is try to limit using it uneccessarily + ## v11.1.4 (date: 23.9.2021) - bugfix: adding concurrencty to catalog check diff --git a/pathlib/functions.go b/pathlib/functions.go index 4e213432..aca11543 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -22,14 +22,12 @@ func Abs(path string) (string, error) { func IsDir(pathname string) bool { stat, err := os.Stat(pathname) - if err != nil { - return false - } - return stat.IsDir() + return err == nil && stat.IsDir() } func IsFile(pathname string) bool { - return Exists(pathname) && !IsDir(pathname) + stat, err := os.Stat(pathname) + return err == nil && !stat.IsDir() } func Size(pathname string) (int64, bool) { From 86f30939c120042ecf9be00afe8dcbc4abedba61 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 27 Sep 2021 14:16:07 +0300 Subject: [PATCH 196/516] RCC-198: documentation updates (v11.1.6) --- common/version.go | 2 +- docs/changelog.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index f895ee9a..355f3c34 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.5` + Version = `v11.1.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0e47f70b..234763df 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,38 @@ # rcc change log +## v11.1.6 (date: 27.9.2021) + +### What to consider when upgrading from series 10 to series 11 of rcc? + +Major version break between rcc 10 and 11 was about removing the old base/live +way of managing environments (`rcc environment ...` commands). That had some +visible changes in rcc commands used. Here is a summary of those changes. + +- Compared to base/live based management of environments, holotree needs + a different mindset to work with. With the new holotree, users decide which + processes share the same working space and which receive their own space. + So, high level management of logical spaces has shifted from rcc to user + (or tools), where in base/live users did not have the option to do so. + Low level management is still rcc responsibility and based on "conda.yaml" + content. +- All `rcc environment` commands were removed or renamed, since this was + an old way of doing things. +- Old `rcc env hash` was renamed to `rcc holotree hash` and changed to show + holotree blueprint hash. +- Old `rcc env plan` was renamed to `rcc holotree plan` and changed to show + plan from given holotree space. +- Old `rcc env cleanup` was renamed to `rcc configuration cleanup` and + changed to work in a way that only holotree things are valid from now on. + This means that if you are using `rcc conf cleanup`, check help for changed + flags also. +- In general, the old `--stage` flag is gone, since it was base/live specific. +- Holotree related commands, including various run commands, now have default + values for the `--space` flag. So if no `--space` flag is given, that + defaults to `user` value, and the same space will be updated based + on requested environment specification. +- Output of some commands have changed, for example there are now more + "Progress" steps in rcc output. + ## v11.1.5 (date: 24.9.2021) - bugfix: performance profiling revealed bottleneck in windows, where calling From 9cc1f3de3c51f75837ad960dfcd576b6b248510d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 29 Sep 2021 16:21:22 +0300 Subject: [PATCH 197/516] RCC-198: documentation updates (v11.2.0) - updated content to [recipes](/docs/recipes.md) about Holotree controls - two new documentation commands, features and usecases, and corresponding markdown documents in docs folder - added env.json capability also into pure `conda.yaml` case in `rcc holotree variables` command (bugfix) --- Rakefile | 2 +- cmd/features.go | 25 ++++++++++++++++++++++ cmd/holotreeVariables.go | 2 ++ cmd/usecases.go | 25 ++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 8 +++++++ docs/features.md | 13 ++++++++++++ docs/recipes.md | 44 +++++++++++++++++++++++++++++++++++++++ docs/usecases.md | 28 +++++++++++++++++++++++++ operations/diagnostics.go | 1 + 10 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 cmd/features.go create mode 100644 cmd/usecases.go create mode 100644 docs/features.md create mode 100644 docs/usecases.md diff --git a/Rakefile b/Rakefile index 7e6788f9..17288c00 100644 --- a/Rakefile +++ b/Rakefile @@ -30,7 +30,7 @@ task :assets do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md docs/recipes.md" + sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md docs/features.md docs/usecases.md docs/recipes.md" end task :clean do diff --git a/cmd/features.go b/cmd/features.go new file mode 100644 index 00000000..01d72d1a --- /dev/null +++ b/cmd/features.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var featuresCmd = &cobra.Command{ + Use: "features", + Short: "Show some of rcc features.", + Long: "Show some of rcc features.", + Run: func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset("docs/features.md") + if err != nil { + pretty.Exit(1, "Cannot show features.md, reason: %v", err) + } + pretty.Page(content) + }, +} + +func init() { + manCmd.AddCommand(featuresCmd) +} diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 4fa20f1b..27c5ade7 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -93,6 +93,8 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp env := conda.EnvironmentExtensionFor(path) if config != nil { env = config.ExecutionEnvironment(path, extra, false) + } else { + env = append(extra, env...) } if Has(workspace) { diff --git a/cmd/usecases.go b/cmd/usecases.go new file mode 100644 index 00000000..97534f90 --- /dev/null +++ b/cmd/usecases.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var usecasesCmd = &cobra.Command{ + Use: "usecases", + Short: "Show some of rcc use cases.", + Long: "Show some of rcc use cases.", + Run: func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset("docs/usecases.md") + if err != nil { + pretty.Exit(1, "Cannot show usecases.md, reason: %v", err) + } + pretty.Page(content) + }, +} + +func init() { + manCmd.AddCommand(usecasesCmd) +} diff --git a/common/version.go b/common/version.go index 355f3c34..73f3d43d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.1.6` + Version = `v11.2.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 234763df..7cb06952 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.2.0 (date: 29.9.2021) + +- updated content to [recipes](/docs/recipes.md) about Holotree controls +- two new documentation commands, features and usecases, and corresponding + markdown documents in docs folder +- added env.json capability also into pure `conda.yaml` case in + `rcc holotree variables` command (bugfix) + ## v11.1.6 (date: 27.9.2021) ### What to consider when upgrading from series 10 to series 11 of rcc? diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 00000000..1997e29f --- /dev/null +++ b/docs/features.md @@ -0,0 +1,13 @@ +# Incomplete list of rcc features + +* supported operating systems are Windows, MacOS, and Linux +* provide repeatable, isolated, and clean environments for robots to run +* automatic environment creation based on declarative conda environment.yaml + files +* easily run software robots (automations) based on declarative robot.yaml files +* test robots in isolated environments before uploading them to Control Room +* provide commands for Robocorp runtime and developer tools (Workforce Agent, + Assistant, Lab, Code, ...) +* provides commands to communicate with Robocorp Control Room from command line +* enable caching dormant environments in efficiently and activating them locally + when required without need to reinstall anything diff --git a/docs/recipes.md b/docs/recipes.md index 17977c7c..f047cbc0 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -220,6 +220,50 @@ mkdir -p output cp target/build/micromamba output/micromamba-$version ``` +## How to control holotree environments? + +There is three controlling factors for where holotree spaces are created. + +First is location of `ROBOCORP_HOME` at creation time of environment. This +decides general location for environment and it cannot be changed or relocated +afterwards. + +Second controlling factor is given using `--controller` option and default for +this is value `user`. And when applications are calling rcc, they should +have their own "controller" identity, so that all spaces created for one +application are groupped together by prefix of their "space" identity name. + +Third controlling factor is content of `--space` option and again default +value there is `user`. Here it is up to user or application to decide their +strategy of use of different names to separate environments to their logical +used partitions. If you choose to use just defaults (user/user) then there +is going to be only one real environment available. + +But above three controls gives you good ways to control how you and your +applications manage their usage of different python environments for +different purposes. You can share environments if you want, but you can also +give dedicates space for thos things that need full control of their space. + +So running following commands demonstrate different levels of control for +space creation. + +``` +export ROBOCORP_HOME=/tmp/rchome +rcc holotree variables simple.yaml +rcc holotree variables --space tips simple.yaml +rcc holotree variables --controller tricks --space tips simple.yaml +``` + +If you now run `rcc holotree list` it should list something like following. + +``` +Identity Controller Space Blueprint Full path +-------- ---------- ----- -------- --------- +5a1fac3c5_2daaa295 rcc.user tips c34ed96c2d8a459a /tmp/rchome/holotree/5a1fac3c5_2daaa295 +5a1fac3c5_9fcd2534 rcc.user user c34ed96c2d8a459a /tmp/rchome/holotree/5a1fac3c5_9fcd2534 +9e7018022_2daaa295 rcc.tricks tips c34ed96c2d8a459a /tmp/rchome/holotree/9e7018022_2daaa295 +``` + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html diff --git a/docs/usecases.md b/docs/usecases.md new file mode 100644 index 00000000..fb9636b0 --- /dev/null +++ b/docs/usecases.md @@ -0,0 +1,28 @@ +# Incomplete list of rcc use cases + +* run robots in Robocorp Workforce Agent locally or in cloud containers +* run robots in Robocorp Assistant +* provide commands for Robocorp Lab and Code to develop robots locally and + communicate to Robocorp Control Room +* provide commands that can be used in CI pipelines (Jenkins, Gitlab CI, ...) + to push robots into Robocorp Control Room +* provide isolated environments to run python scripts and applications +* to use other scripting languages and tools available from conda-forge (or + conda in general) with isolated and easily installed manner (see list below + for ideas what is available) + +## What is available from conda-forge? + +* python and libraries +* ruby and libraries +* perl and libraries +* lua and libraries +* r and libraries +* julia and libraries +* make, cmake and compilers (C++, Fortran, ...) +* nodejs +* rust +* php +* gawk, sed, and emacs, vim +* ROS libraries (robot operating system) +* firefox diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 59c36ef3..f2aea3a1 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -61,6 +61,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["controller"] = common.ControllerIdentity() result.Details["user-agent"] = common.UserAgent() result.Details["installationId"] = xviper.TrackingIdentity() + result.Details["telemetry-enabled"] = fmt.Sprintf("%v", xviper.CanTrack()) result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") From b34f484bff96c471ddbabddfbdb7dfb6480ba869 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Oct 2021 10:54:50 +0300 Subject: [PATCH 198/516] RCC-200: automatic template updates (v11.3.0) - update robot templates from cloud (not used yet, coming up in next versions) --- assets/settings.yaml | 1 + common/version.go | 2 +- docs/changelog.md | 4 ++ operations/initialize.go | 99 ++++++++++++++++++++++++++++++++++++++++ settings/settings.go | 6 +++ 5 files changed, 111 insertions(+), 1 deletion(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index cbb93daa..a903571f 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -20,6 +20,7 @@ autoupdates: assistant: https://downloads.robocorp.com/assistant/releases/ workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ lab: https://downloads.robocorp.com/lab/releases/ + templates: https://downloads.robocorp.com/templates/templates.yaml certificates: verify-ssl: true diff --git a/common/version.go b/common/version.go index 73f3d43d..4bdf5ab7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.2.0` + Version = `v11.3.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7cb06952..701a14d8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.3.0 (date: 4.10.2021) + +- update robot templates from cloud (not used yet, coming up in next versions) + ## v11.2.0 (date: 29.9.2021) - updated content to [recipes](/docs/recipes.md) about Holotree controls diff --git a/operations/initialize.go b/operations/initialize.go index 0364c7cb..142f37ce 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -4,15 +4,110 @@ import ( "archive/zip" "bytes" "fmt" + "os" "path/filepath" "sort" "strings" "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" + "gopkg.in/yaml.v1" ) +type StringMap map[string]string + +type MetaTemplates struct { + Date string `yaml:"date"` + Hash string `yaml:"hash"` + Templates StringMap `yaml:"templates"` + Url string `yaml:"url"` +} + +func TemplateInfo(filename string) (ingore *MetaTemplates, err error) { + defer fail.Around(&err) + + raw, err := os.ReadFile(filename) + fail.On(err != nil, "Failure reading %q, reason: %v", filename, err) + var metadata MetaTemplates + err = yaml.Unmarshal(raw, &metadata) + fail.On(err != nil, "Failure parsing %q, reason: %v", filename, err) + return &metadata, nil +} + +func templatesYamlPart() string { + return filepath.Join(common.TemplateLocation(), "templates.yaml.part") +} + +func templatesYamlFinal() string { + return filepath.Join(common.TemplateLocation(), "templates.yaml") +} + +func templatesZipPart() string { + return filepath.Join(common.TemplateLocation(), "templates.zip.part") +} + +func templatesZipFinal() string { + return filepath.Join(common.TemplateLocation(), "templates.zip") +} + +func needNewTemplates() (ignore *MetaTemplates, err error) { + defer fail.Around(&err) + + metadata := settings.Global.TemplatesYamlURL() + if len(metadata) == 0 { + common.Debug("No URL for templates.yaml available.") + return nil, nil + } + partfile := templatesYamlPart() + err = cloud.Download(metadata, partfile) + fail.On(err != nil, "Failure loading %q, reason: %s", metadata, err) + meta, err := TemplateInfo(partfile) + fail.On(err != nil, "%s", err) + fail.On(!strings.HasPrefix(meta.Url, "https:"), "Location for templates.zip is not https: %q", meta.Url) + hash, err := pathlib.Sha256(templatesZipFinal()) + if err != nil || hash != meta.Hash { + return meta, nil + } + return nil, nil +} + +func downloadTemplatesZip(meta *MetaTemplates) (err error) { + defer fail.Around(&err) + + partfile := templatesZipPart() + err = cloud.Download(meta.Url, partfile) + fail.On(err != nil, "Failure loading %q, reason: %s", meta.Url, err) + hash, err := pathlib.Sha256(partfile) + fail.On(err != nil, "Failure hashing %q, reason: %s", partfile, err) + fail.On(hash != meta.Hash, "Received broken templates.zip from %q", meta.Hash) + return nil +} + +func updateTemplates() (err error) { + defer fail.Around(&err) + + defer os.Remove(templatesZipPart()) + defer os.Remove(templatesYamlPart()) + + meta, err := needNewTemplates() + fail.On(err != nil, "%s", err) + if meta == nil { + return nil + } + err = downloadTemplatesZip(meta) + fail.On(err != nil, "%s", err) + err = os.Rename(templatesYamlPart(), templatesYamlFinal()) + alt := os.Rename(templatesZipPart(), templatesZipFinal()) + fail.On(alt != nil, "%s", alt) + fail.On(err != nil, "%s", err) + return nil +} + func unpack(content []byte, directory string) error { common.Debug("Initializing:") size := int64(len(content)) @@ -41,6 +136,10 @@ func unpack(content []byte, directory string) error { } func ListTemplates() []string { + err := updateTemplates() + if err != nil { + pretty.Warning("Problem updating templates.zip, reason: %v", err) + } assets := blobs.AssetNames() result := make([]string, 0, len(assets)) for _, name := range blobs.AssetNames() { diff --git a/settings/settings.go b/settings/settings.go index 82a53872..5327e37a 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -109,6 +109,12 @@ func resolveLink(link, page string) string { type gateway bool +func (it gateway) TemplatesYamlURL() string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Autoupdates["templates"] +} + func (it gateway) Diagnostics(target *common.DiagnosticStatus) { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) From e8c32fdb4daba3c975a276679e287054e1cb8865 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Oct 2021 15:28:24 +0300 Subject: [PATCH 199/516] RCC-200: automatic template updates (v11.3.1) - using templates from templates.zip in addition to internal templates - command `rcc holotree bootstrap` update to use templates.zip - command `rcc interactive create` now uses template descriptions - command `rcc robot init` now has `--json` flag to produce template list as JSON - settings.yaml updated to version 2021.10 --- assets/settings.yaml | 7 +-- assets/templates.yaml | 4 ++ blobs/asset_test.go | 3 +- cmd/cloudPrepare.go | 2 +- cmd/holotreeBootstrap.go | 29 ++++------ cmd/holotreeVariables.go | 2 +- cmd/initialize.go | 24 ++++++++- common/variables.go | 4 ++ common/version.go | 2 +- docs/changelog.md | 9 ++++ htfs/commands.go | 39 +++++--------- htfs/directory.go | 2 +- htfs/fs_test.go | 3 +- htfs/library.go | 2 +- htfs/ziplibrary.go | 2 +- operations/diagnostics.go | 2 +- operations/initialize.go | 102 ++++++++++++++++++++++++++++++------ operations/issues.go | 3 +- operations/running.go | 2 +- operations/zipper.go | 22 ++++++++ robot/robot.go | 10 ++-- robot_tests/fullrun.robot | 6 +-- robot_tests/templates.robot | 6 +-- settings/data.go | 13 +++-- settings/settings.go | 6 --- wizard/create.go | 13 ++++- 26 files changed, 210 insertions(+), 109 deletions(-) create mode 100644 assets/templates.yaml diff --git a/assets/settings.yaml b/assets/settings.yaml index a903571f..f0b3bd83 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -29,11 +29,6 @@ branding: logo: https://downloads.robocorp.com/company/press-kit/logos/robocorp-logo-black.svg theme-color: FF0000 -templates: - standard: Standard Robot Framework template - extended: Extended Robot Framework template - python: Basic Python template - meta: source: builtin - version: 2021.04 + version: 2021.10 diff --git a/assets/templates.yaml b/assets/templates.yaml new file mode 100644 index 00000000..d22ca4ad --- /dev/null +++ b/assets/templates.yaml @@ -0,0 +1,4 @@ +templates: + standard: Standard Robot Framework template + extended: Extended Robot Framework template + python: Basic Python template diff --git a/blobs/asset_test.go b/blobs/asset_test.go index 27429e3c..f243a195 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -12,6 +12,7 @@ func TestCanSeeBaseZipAsset(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) must_be.Panic(func() { blobs.MustAsset("assets/missing.zip") }) + wont_be.Panic(func() { blobs.MustAsset("assets/templates.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/standard.zip") }) wont_be.Panic(func() { blobs.MustAsset("assets/python.zip") }) wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) @@ -27,7 +28,7 @@ func TestCanSeeBaseZipAsset(t *testing.T) { func TestCanGetTemplateNamesThruOperations(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - assets := operations.ListTemplates() + assets := operations.ListTemplates(true) wont_be.Nil(assets) must_be.True(len(assets) == 3) must_be.Equal([]string{"extended", "python", "standard"}, assets) diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index f1ff0864..83708be2 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -53,7 +53,7 @@ var prepareCloudCmd = &cobra.Command{ var label string condafile := config.CondaConfigFile() - label, err = htfs.NewEnvironment(false, condafile, config.Holozip()) + label, err = htfs.NewEnvironment(condafile, config.Holozip(), true, false) pretty.Guard(err == nil, 8, "Error: %v", err) common.Log("Prepared %q.", label) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index ecd12e70..856b15a4 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -5,13 +5,11 @@ import ( "os" "path/filepath" - "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" - "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -20,22 +18,25 @@ var ( ) func updateEnvironments(robots []string) { - for at, robotling := range robots { + tree, err := htfs.New() + pretty.Guard(err == nil, 2, "Holotree creation error: %v", err) + for at, template := range robots { workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) - err := operations.Unzip(workarea, robotling, false, true) - pretty.Guard(err == nil, 2, "Could not unzip %q, reason: %w", robotling, err) + err = operations.InitializeWorkarea(workarea, template, false, forceFlag) + pretty.Guard(err == nil, 2, "Could not create robot %q, reason: %v", template, err) targetRobot := robot.DetectConfigurationName(workarea) + _, blueprint, err := htfs.ComposeFinalBlueprint([]string{}, targetRobot) + if tree.HasBlueprint(blueprint) { + continue + } config, err := robot.LoadRobotYaml(targetRobot, false) pretty.Guard(err == nil, 2, "Could not load robot config %q, reason: %w", targetRobot, err) if !config.UsesConda() { continue } - condafile := config.CondaConfigFile() - tree, err := htfs.New() - pretty.Guard(err == nil, 2, "Holotree creation error: %v", err) - err = htfs.RecordCondaEnvironment(tree, condafile, false) + _, err = htfs.NewEnvironment(config.CondaConfigFile(), "", false, false) pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) } } @@ -51,15 +52,7 @@ var holotreeBootstrapCmd = &cobra.Command{ defer common.Stopwatch("Holotree bootstrap lasted").Report() } - robots := make([]string, 0, 20) - for key, _ := range settings.Global.Templates() { - zipname := fmt.Sprintf("%s.zip", key) - filename := filepath.Join(common.TemplateLocation(), zipname) - robots = append(robots, filename) - url := fmt.Sprintf("templates/%s", zipname) - err := cloud.Download(settings.Global.DownloadsLink(url), filename) - pretty.Guard(err == nil, 2, "Could not download %q, reason: %w", url, err) - } + robots := operations.ListTemplates(false) if !holotreeQuick { updateEnvironments(robots) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 27c5ade7..a4742613 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -78,7 +78,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp if config != nil { holozip = config.Holozip() } - path, err := htfs.NewEnvironment(force, condafile, holozip) + path, err := htfs.NewEnvironment(condafile, holozip, true, force) pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/cmd/initialize.go b/cmd/initialize.go index bd5b9456..71d18c98 100644 --- a/cmd/initialize.go +++ b/cmd/initialize.go @@ -8,19 +8,33 @@ import ( "github.com/spf13/cobra" ) +var ( + internalOnlyFlag bool +) + func createWorkarea() { if len(directory) == 0 { pretty.Exit(1, "Error: missing target directory") } - err := operations.InitializeWorkarea(directory, templateName, forceFlag) + err := operations.InitializeWorkarea(directory, templateName, internalOnlyFlag, forceFlag) if err != nil { pretty.Exit(2, "Error: %v", err) } } +func listJsonTemplates() { + templates := make(map[string]string) + for _, pair := range operations.ListTemplatesWithDescription(internalOnlyFlag) { + templates[pair[0]] = pair[1] + } + out, err := operations.NiceJsonOutput(templates) + pretty.Guard(err == nil, 2, "Failed to format templates as JSON, reason: %s", err) + common.Stdout("%s\n", out) +} + func listTemplates() { common.Stdout("Template names:\n") - for _, name := range operations.ListTemplates() { + for _, name := range operations.ListTemplates(internalOnlyFlag) { common.Stdout("- %v\n", name) } } @@ -34,6 +48,10 @@ var initializeCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Initialization lasted").Report() } + if jsonFlag { + listJsonTemplates() + return + } if listFlag { listTemplates() } else { @@ -49,4 +67,6 @@ func init() { initializeCmd.Flags().StringVarP(&templateName, "template", "t", "standard", "Template to use to generate the robot content.") initializeCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force the creation of the robot and possibly overwrite data.") initializeCmd.Flags().BoolVarP(&listFlag, "list", "l", false, "List available templates.") + initializeCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "List available templates as JSON.") + initializeCmd.Flags().BoolVarP(&internalOnlyFlag, "internal", "i", false, "Use only builtin internal templates.") } diff --git a/common/variables.go b/common/variables.go index a6e1ad9a..b467075e 100644 --- a/common/variables.go +++ b/common/variables.go @@ -155,6 +155,10 @@ func ForceDebug() { UnifyVerbosityFlags() } +func Platform() string { + return strings.ToLower(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) +} + func UserAgent() string { return fmt.Sprintf("rcc/%s (%s %s) %s", Version, runtime.GOOS, runtime.GOARCH, ControllerIdentity()) } diff --git a/common/version.go b/common/version.go index 4bdf5ab7..1d6596be 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.0` + Version = `v11.3.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 701a14d8..f2aa5249 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.3.1 (date: 5.10.2021) + +- using templates from templates.zip in addition to internal templates +- command `rcc holotree bootstrap` update to use templates.zip +- command `rcc interactive create` now uses template descriptions +- command `rcc robot init` now has `--json` flag to produce template list + as JSON +- settings.yaml updated to version 2021.10 + ## v11.3.0 (date: 4.10.2021) - update robot templates from cloud (not used yet, coming up in next versions) diff --git a/htfs/commands.go b/htfs/commands.go index 17c5205c..2a842cf7 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -1,11 +1,9 @@ package htfs import ( - "fmt" "io/ioutil" "os" "path/filepath" - "runtime" "strings" "github.com/robocorp/rcc/anywork" @@ -17,11 +15,7 @@ import ( "github.com/robocorp/rcc/xviper" ) -func Platform() string { - return strings.ToLower(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) -} - -func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { +func NewEnvironment(condafile, holozip string, restore, force bool) (label string, err error) { defer fail.Around(&err) defer common.Progress(12, "Fresh holotree done [with %d workers].", anywork.Scale()) @@ -58,28 +52,17 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er library = tree } - common.Progress(11, "Restore space from library [with %d workers].", anywork.Scale()) - path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) - fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + path := "" + if restore { + common.Progress(11, "Restore space from library [with %d workers].", anywork.Scale()) + path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + } else { + common.Progress(11, "Restoring space skipped.") + } return path, nil } -func RecordCondaEnvironment(tree MutableLibrary, condafile string, force bool) (err error) { - defer fail.Around(&err) - - locker, err := pathlib.Locker(common.HolotreeLock(), 30000) - fail.On(err != nil, "Could not get lock for holotree. Quiting.") - defer locker.Release() - - right, err := conda.ReadCondaYaml(condafile) - fail.On(err != nil, "Could not load environmet config %q, reason: %w", condafile, err) - - content, err := right.AsYaml() - fail.On(err != nil, "YAML error with %q, reason: %w", condafile, err) - - return RecordEnvironment(tree, []byte(content), force) -} - func CleanupHolotreeStage(tree MutableLibrary) error { common.Timeline("holotree stage removal start") defer common.Timeline("holotree stage removal done") @@ -91,7 +74,11 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e // following must be setup here common.StageFolder = tree.Stage() + backup := common.Liveonly common.Liveonly = true + defer func() { + common.Liveonly = backup + }() common.Debug("Holotree stage is %q.", tree.Stage()) exists := tree.HasBlueprint(blueprint) diff --git a/htfs/directory.go b/htfs/directory.go index 026d705f..9aa86b14 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -54,7 +54,7 @@ func NewRoot(path string) (*Root, error) { return &Root{ Identity: basename, Path: fullpath, - Platform: Platform(), + Platform: common.Platform(), Lifted: false, Tree: newDir(""), }, nil diff --git a/htfs/fs_test.go b/htfs/fs_test.go index 2c66b41a..8df5f404 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/hamlet" "github.com/robocorp/rcc/htfs" ) @@ -45,7 +46,7 @@ func TestHTFSspecification(t *testing.T) { func TestZipLibrary(t *testing.T) { must, wont := hamlet.Specifications(t) - must.Equal("linux_amd64", htfs.Platform()) + must.Equal("linux_amd64", common.Platform()) _, blueprint, err := htfs.ComposeFinalBlueprint([]string{"testdata/simple.yaml"}, "") must.Nil(err) diff --git a/htfs/library.go b/htfs/library.go index 66b766c1..c1913d04 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -186,7 +186,7 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - name := fmt.Sprintf("%s.%s", key, Platform()) + name := fmt.Sprintf("%s.%s", key, common.Platform()) return filepath.Join(common.HololibCatalogLocation(), name) } diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 65d0bab3..c2580f5c 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -71,7 +71,7 @@ func (it *ziplibrary) Open(digest string) (readable io.Reader, closer Closer, er } func (it *ziplibrary) CatalogPath(key string) string { - return filepath.Join("catalog", fmt.Sprintf("%s.%s", key, Platform())) + return filepath.Join("catalog", fmt.Sprintf("%s.%s", key, common.Platform())) } func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index f2aea3a1..4515ffc0 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -62,7 +62,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["user-agent"] = common.UserAgent() result.Details["installationId"] = xviper.TrackingIdentity() result.Details["telemetry-enabled"] = fmt.Sprintf("%v", xviper.CanTrack()) - result.Details["os"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) + result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") diff --git a/operations/initialize.go b/operations/initialize.go index 142f37ce..f60df646 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -20,6 +20,8 @@ import ( ) type StringMap map[string]string +type StringPair [2]string +type StringPairList []StringPair type MetaTemplates struct { Date string `yaml:"date"` @@ -28,15 +30,35 @@ type MetaTemplates struct { Url string `yaml:"url"` } +func (it StringPairList) Len() int { + return len(it) +} + +func (it StringPairList) Less(left, right int) bool { + return it[left][0] < it[right][0] +} + +func (it StringPairList) Swap(left, right int) { + it[left], it[right] = it[right], it[left] +} + +func parseTemplateInfo(raw []byte) (ingore *MetaTemplates, err error) { + var metadata MetaTemplates + err = yaml.Unmarshal(raw, &metadata) + if err != nil { + return nil, err + } + return &metadata, nil +} + func TemplateInfo(filename string) (ingore *MetaTemplates, err error) { defer fail.Around(&err) raw, err := os.ReadFile(filename) fail.On(err != nil, "Failure reading %q, reason: %v", filename, err) - var metadata MetaTemplates - err = yaml.Unmarshal(raw, &metadata) + metadata, err := parseTemplateInfo(raw) fail.On(err != nil, "Failure parsing %q, reason: %v", filename, err) - return &metadata, nil + return metadata, nil } func templatesYamlPart() string { @@ -51,7 +73,7 @@ func templatesZipPart() string { return filepath.Join(common.TemplateLocation(), "templates.zip.part") } -func templatesZipFinal() string { +func TemplatesZip() string { return filepath.Join(common.TemplateLocation(), "templates.zip") } @@ -69,13 +91,27 @@ func needNewTemplates() (ignore *MetaTemplates, err error) { meta, err := TemplateInfo(partfile) fail.On(err != nil, "%s", err) fail.On(!strings.HasPrefix(meta.Url, "https:"), "Location for templates.zip is not https: %q", meta.Url) - hash, err := pathlib.Sha256(templatesZipFinal()) + hash, err := pathlib.Sha256(TemplatesZip()) if err != nil || hash != meta.Hash { return meta, nil } return nil, nil } +func activeTemplateInfo(internal bool) (*MetaTemplates, error) { + if !internal { + meta, err := TemplateInfo(templatesYamlFinal()) + if err == nil { + return meta, nil + } + } + raw, err := blobs.Asset("assets/templates.yaml") + if err != nil { + return nil, err + } + return parseTemplateInfo(raw) +} + func downloadTemplatesZip(meta *MetaTemplates) (err error) { defer fail.Around(&err) @@ -102,7 +138,7 @@ func updateTemplates() (err error) { err = downloadTemplatesZip(meta) fail.On(err != nil, "%s", err) err = os.Rename(templatesYamlPart(), templatesYamlFinal()) - alt := os.Rename(templatesZipPart(), templatesZipFinal()) + alt := os.Rename(templatesZipPart(), TemplatesZip()) fail.On(alt != nil, "%s", alt) fail.On(err != nil, "%s", err) return nil @@ -135,25 +171,57 @@ func unpack(content []byte, directory string) error { return nil } -func ListTemplates() []string { +func ListTemplatesWithDescription(internal bool) StringPairList { err := updateTemplates() if err != nil { pretty.Warning("Problem updating templates.zip, reason: %v", err) } - assets := blobs.AssetNames() - result := make([]string, 0, len(assets)) - for _, name := range blobs.AssetNames() { - if !strings.HasPrefix(name, "assets") || !strings.HasSuffix(name, ".zip") { - continue - } - result = append(result, strings.TrimSuffix(filepath.Base(name), filepath.Ext(name))) + result := make(StringPairList, 0, 10) + meta, err := activeTemplateInfo(internal) + if err != nil { + pretty.Warning("Problem getting template list, reason: %v", err) + return result + } + for name, description := range meta.Templates { + result = append(result, StringPair{name, description}) } - sort.Strings(result) + sort.Sort(result) return result } -func InitializeWorkarea(directory, name string, force bool) error { - content, err := blobs.Asset(fmt.Sprintf("assets/%s.zip", name)) +func ListTemplates(internal bool) []string { + pairs := ListTemplatesWithDescription(internal) + result := make([]string, 0, len(pairs)) + for _, pair := range pairs { + result = append(result, pair[0]) + } + return result +} + +func templateByName(name string, internal bool) ([]byte, error) { + zipfile := TemplatesZip() + blobname := fmt.Sprintf("assets/%s.zip", name) + if internal || !pathlib.IsFile(zipfile) { + return blobs.Asset(blobname) + } + unzipper, err := newUnzipper(zipfile) + if err != nil { + return nil, err + } + defer unzipper.Close() + zipname := fmt.Sprintf("%s.zip", name) + blob, err := unzipper.Asset(zipname) + if err != nil { + return nil, err + } + if blob != nil { + return blob, nil + } + return blobs.Asset(blobname) +} + +func InitializeWorkarea(directory, name string, internal, force bool) error { + content, err := templateByName(name, internal) if err != nil { return err } diff --git a/operations/issues.go b/operations/issues.go index 666cb6f8..ce4b53b8 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "os" "path/filepath" - "runtime" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -109,7 +108,7 @@ func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, token["controller"] = common.ControllerIdentity() _, ok = token["platform"] if !ok { - token["platform"] = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH) + token["platform"] = common.Platform() } issueReport, err := token.AsJson() if err != nil { diff --git a/operations/running.go b/operations/running.go index 42fa548b..e900c9a5 100644 --- a/operations/running.go +++ b/operations/running.go @@ -114,7 +114,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. return true, config, todo, "" } - label, err := htfs.NewEnvironment(force, config.CondaConfigFile(), config.Holozip()) + label, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/operations/zipper.go b/operations/zipper.go index d03c5e79..eec29aeb 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" ) @@ -127,6 +128,27 @@ func (it *unzipper) Explode(workers int, directory string) error { return nil } +func (it *unzipper) Asset(name string) ([]byte, error) { + stream, err := it.reader.Open(name) + if err != nil { + return nil, err + } + defer stream.Close() + stat, err := stream.Stat() + if err != nil { + return nil, err + } + payload := make([]byte, stat.Size()) + total, err := stream.Read(payload) + if err != nil && err != io.EOF { + return nil, err + } + if int64(total) != stat.Size() { + pretty.Warning("Asset %q read partially!", name) + } + return payload, nil +} + func (it *unzipper) Extract(directory string) error { common.Debug("Extracting:") success := true diff --git a/robot/robot.go b/robot/robot.go index 02328f03..34a2828c 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -310,11 +310,11 @@ func (it *robot) TaskByName(name string) Task { } func (it *robot) UsesConda() bool { - return len(it.Conda) > 0 || len(it.availableEnvironmentConfigurations(osArchitectureToken())) > 0 + return len(it.Conda) > 0 || len(it.availableEnvironmentConfigurations(common.Platform())) > 0 } func (it *robot) CondaConfigFile() string { - available := it.availableEnvironmentConfigurations(osArchitectureToken()) + available := it.availableEnvironmentConfigurations(common.Platform()) if len(available) > 0 { return available[0] } @@ -325,12 +325,8 @@ func (it *robot) WorkingDirectory() string { return it.Root } -func osArchitectureToken() string { - return fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) -} - func freezeFileBasename() string { - return fmt.Sprintf("environment_%s_freeze.yaml", osArchitectureToken()) + return fmt.Sprintf("environment_%s_freeze.yaml", common.Platform()) } func submatch(pattern *regexp.Regexp, expected, text string) bool { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 06652f7d..1eb0b199 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -48,7 +48,7 @@ Goal: Show config help for rcc. Must Have credentials Goal: List available robot templates. - Step build/rcc robot init -l --controller citests + Step build/rcc robot init -i -l --controller citests Must Have extended Must Have python Must Have standard @@ -56,7 +56,7 @@ Goal: List available robot templates. Must Have OK. Goal: Initialize new standard robot into tmp/fluffy folder using force. - Step build/rcc robot init --controller citests -t extended -d tmp/fluffy -f + Step build/rcc robot init -i --controller citests -t extended -d tmp/fluffy -f Use STDERR Must Have OK. @@ -67,7 +67,7 @@ Goal: There should now be fluffy in robot listing Must Have "robot" Goal: Fail to initialize new standard robot into tmp/fluffy without force. - Step build/rcc robot init --controller citests -t extended -d tmp/fluffy 2 + Step build/rcc robot init -i --controller citests -t extended -d tmp/fluffy 2 Use STDERR Must Have Error: Directory Must Have fluffy is not empty diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index bb31a09a..78af7468 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -6,7 +6,7 @@ Resource resources.robot *** Test cases *** Goal: Initialize new standard robot. - Step build/rcc robot init --controller citests -t standard -d tmp/standardi -f + Step build/rcc robot init -i --controller citests -t standard -d tmp/standardi -f Use STDERR Must Have OK. @@ -20,7 +20,7 @@ Goal: Running standard robot is succesful. Must Have OK. Goal: Initialize new python robot. - Step build/rcc robot init --controller citests -t python -d tmp/pythoni -f + Step build/rcc robot init -i --controller citests -t python -d tmp/pythoni -f Use STDERR Must Have OK. @@ -34,7 +34,7 @@ Goal: Running python robot is succesful. Must Have OK. Goal: Initialize new extended robot. - Step build/rcc robot init --controller citests -t extended -d tmp/extendedi -f + Step build/rcc robot init -i --controller citests -t extended -d tmp/extendedi -f Use STDERR Must Have OK. diff --git a/settings/data.go b/settings/data.go index a29e9939..892cc22d 100644 --- a/settings/data.go +++ b/settings/data.go @@ -17,13 +17,12 @@ const ( type StringMap map[string]string type Settings struct { - Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` - Branding StringMap `yaml:"branding" json:"branding"` - Certificates *Certificates `yaml:"certificates" json:"certificates"` - Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` - Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` - Templates map[string]string `yaml:"templates" json:"templates"` - Meta *Meta `yaml:"meta" json:"meta"` + Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` + Branding StringMap `yaml:"branding" json:"branding"` + Certificates *Certificates `yaml:"certificates" json:"certificates"` + Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` + Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` + Meta *Meta `yaml:"meta" json:"meta"` } func FromBytes(raw []byte) (*Settings, error) { diff --git a/settings/settings.go b/settings/settings.go index 5327e37a..a2383956 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -166,12 +166,6 @@ func (it gateway) Hostnames() []string { return config.Hostnames() } -func (it gateway) Templates() map[string]string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Templates -} - func (it gateway) ConfiguredHttpTransport() *http.Transport { return httpTransport } diff --git a/wizard/create.go b/wizard/create.go index 484887f4..0532e21f 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strconv" "strings" @@ -89,12 +90,20 @@ func Create(arguments []string) error { return fmt.Errorf("Folder %s already exists. Try with other name.", robotName) } - selected, err := choose("Choose a template", "Templates", operations.ListTemplates()) + templates := operations.ListTemplatesWithDescription(false) + descriptions := make([]string, 0, len(templates)) + lookup := make(map[string]string) + for _, template := range templates { + descriptions = append(descriptions, template[1]) + lookup[template[1]] = template[0] + } + sort.Strings(descriptions) + selected, err := choose("Choose a template", "Templates", descriptions) if err != nil { return err } - err = operations.InitializeWorkarea(fullpath, selected, false) + err = operations.InitializeWorkarea(fullpath, lookup[selected], false, false) if err != nil { return err } From eb4dc4517bf60c2f411af83e357bcfa2c1adf313 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 7 Oct 2021 10:05:58 +0300 Subject: [PATCH 200/516] RCC-200: automatic template updates + bug fixes (v11.3.2) - templates are removed when quick cleanup is requested - bugfix: now debug and trace flags are also considered same as `VERBOSE_ENVIRONMENT_BUILDING` environment variable - bugfix: added some jupyter paths as skipped ingored ones in diagnostics - added canary checks into diagnostics for pypi and conda repos --- cloud/client.go | 5 +++ common/variables.go | 2 +- common/version.go | 2 +- conda/cleanup.go | 2 ++ docs/changelog.md | 8 +++++ mocks/client.go | 4 +++ operations/diagnostics.go | 75 ++++++++++++++++++++++++++++++++++++--- settings/settings.go | 21 +++++++++++ 8 files changed, 112 insertions(+), 7 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index 82ce159c..a537412b 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -41,6 +41,7 @@ type Response struct { type Client interface { Endpoint() string NewRequest(string) *Request + Head(request *Request) *Response Get(request *Request) *Response Post(request *Request) *Response Put(request *Request) *Response @@ -139,6 +140,10 @@ func (it *internalClient) NewRequest(url string) *Request { } } +func (it *internalClient) Head(request *Request) *Response { + return it.does("HEAD", request) +} + func (it *internalClient) Get(request *Request) *Response { return it.does("GET", request) } diff --git a/common/variables.go b/common/variables.go index b467075e..60f775b6 100644 --- a/common/variables.go +++ b/common/variables.go @@ -62,7 +62,7 @@ func RobocorpLock() string { } func VerboseEnvironmentBuilding() bool { - return len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 + return DebugFlag || TraceFlag || len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 } func OverrideSystemRequirements() bool { diff --git a/common/version.go b/common/version.go index 1d6596be..0b67cbd4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.1` + Version = `v11.3.2` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index a6cd2bdb..908019c1 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -59,11 +59,13 @@ func alwaysCleanup(dryrun bool) { func quickCleanup(dryrun bool) error { if dryrun { + common.Log("- %v", common.TemplateLocation()) common.Log("- %v", common.PipCache()) common.Log("- %v", common.HolotreeLocation()) common.Log("- %v", common.RobocorpTempRoot()) return nil } + safeRemove("templates", common.TemplateLocation()) safeRemove("cache", common.PipCache()) err := safeRemove("cache", common.HolotreeLocation()) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index f2aa5249..bd9bd5d8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.3.2 (date: 7.10.2021) + +- templates are removed when quick cleanup is requested +- bugfix: now debug and trace flags are also considered same as + `VERBOSE_ENVIRONMENT_BUILDING` environment variable +- bugfix: added some jupyter paths as skipped ingored ones in diagnostics +- added canary checks into diagnostics for pypi and conda repos + ## v11.3.1 (date: 5.10.2021) - using templates from templates.zip in addition to internal templates diff --git a/mocks/client.go b/mocks/client.go index 8916340d..6c2b35d7 100644 --- a/mocks/client.go +++ b/mocks/client.go @@ -48,6 +48,10 @@ func (it *MockClient) does(method string, request *cloud.Request) *cloud.Respons return it.Responses[index] } +func (it *MockClient) Head(request *cloud.Request) *cloud.Response { + return it.does("HEAD", request) +} + func (it *MockClient) Get(request *cloud.Request) *cloud.Response { return it.does("GET", request) } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 4515ffc0..180b9bae 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -26,11 +26,13 @@ import ( ) const ( - canaryUrl = `/canary.txt` - statusOk = `ok` - statusWarning = `warning` - statusFail = `fail` - statusFatal = `fatal` + canaryUrl = `/canary.txt` + pypiCanaryUrl = `/jupyterlab-pygments/` + condaCanaryUrl = `/conda-forge/linux-64/repodata.json` + statusOk = `ok` + statusWarning = `warning` + statusFail = `fail` + statusFatal = `fatal` ) type stringerr func() (string, error) @@ -84,6 +86,8 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Checks = append(result.Checks, dnsLookupCheck(host)) } result.Checks = append(result.Checks, canaryDownloadCheck()) + result.Checks = append(result.Checks, pypiHeadCheck()) + result.Checks = append(result.Checks, condaHeadCheck()) return result } @@ -171,6 +175,64 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { } } +func condaHeadCheck() *common.DiagnosticCheck { + supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") + client, err := cloud.NewClient(settings.Global.CondaLink("")) + if err != nil { + return &common.DiagnosticCheck{ + Type: "network", + Status: statusWarning, + Message: fmt.Sprintf("%v: %v", settings.Global.CondaLink(""), err), + Link: supportNetworkUrl, + } + } + request := client.NewRequest(condaCanaryUrl) + response := client.Head(request) + if response.Status >= 400 { + return &common.DiagnosticCheck{ + Type: "network", + Status: statusWarning, + Message: fmt.Sprintf("Conda canary download failed: %d", response.Status), + Link: supportNetworkUrl, + } + } + return &common.DiagnosticCheck{ + Type: "network", + Status: statusOk, + Message: fmt.Sprintf("Conda canary download successful: %s", settings.Global.CondaLink(condaCanaryUrl)), + Link: supportNetworkUrl, + } +} + +func pypiHeadCheck() *common.DiagnosticCheck { + supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") + client, err := cloud.NewClient(settings.Global.PypiLink("")) + if err != nil { + return &common.DiagnosticCheck{ + Type: "network", + Status: statusWarning, + Message: fmt.Sprintf("%v: %v", settings.Global.PypiLink(""), err), + Link: supportNetworkUrl, + } + } + request := client.NewRequest(pypiCanaryUrl) + response := client.Head(request) + if response.Status >= 400 { + return &common.DiagnosticCheck{ + Type: "network", + Status: statusWarning, + Message: fmt.Sprintf("PyPI canary download failed: %d", response.Status), + Link: supportNetworkUrl, + } + } + return &common.DiagnosticCheck{ + Type: "network", + Status: statusOk, + Message: fmt.Sprintf("PyPI canary download successful: %s", settings.Global.PypiLink(pypiCanaryUrl)), + Link: supportNetworkUrl, + } +} + func canaryDownloadCheck() *common.DiagnosticCheck { supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") client, err := cloud.NewClient(settings.Global.DownloadsLink("")) @@ -268,6 +330,9 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str for _, tail := range paths { investigated = true fullpath := filepath.Join(rootdir, tail) + if strings.Contains(fullpath, ".ipynb_checkpoints") || strings.Contains(fullpath, ".virtual_documents") { + continue + } content, err := ioutil.ReadFile(fullpath) if err != nil { diagnose.Fail(supportGeneralUrl, "Problem reading %s file %q: %v", label, tail, err) diff --git a/settings/settings.go b/settings/settings.go index a2383956..25f04ade 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -16,6 +16,11 @@ import ( "github.com/robocorp/rcc/pretty" ) +const ( + pypiDefault = "https://pypi.org/simple/" + condaDefault = "https://conda.anaconda.org/" +) + var ( httpTransport *http.Transport cachedSettings *Settings @@ -160,6 +165,22 @@ func (it gateway) DocsLink(page string) string { return resolveLink(it.Endpoints().Docs, page) } +func (it gateway) PypiLink(page string) string { + endpoint := it.Endpoints().Pypi + if len(endpoint) == 0 { + endpoint = pypiDefault + } + return resolveLink(endpoint, page) +} + +func (it gateway) CondaLink(page string) string { + endpoint := it.Endpoints().Conda + if len(endpoint) == 0 { + endpoint = condaDefault + } + return resolveLink(endpoint, page) +} + func (it gateway) Hostnames() []string { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) From f1e1982a672026202c0c1cfaab98d847820afe00 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 8 Oct 2021 11:57:07 +0300 Subject: [PATCH 201/516] RCC-201: micromamba upgrade (v11.3.3) - micromamba update to version 0.16.0 - minor change on os.Stat usage in holotree functions - changed minimum required worker count to 2 (was 4 previously) --- anywork/worker.go | 4 ++-- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 6 ++++++ htfs/functions.go | 6 +++--- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/anywork/worker.go b/anywork/worker.go index cfd0f236..53918381 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -77,8 +77,8 @@ func AutoScale() { if limit > 96 { limit = 96 } - if limit < 4 { - limit = 4 + if limit < 2 { + limit = 2 } for headcount < limit { go member(headcount) diff --git a/common/version.go b/common/version.go index 0b67cbd4..df0b1ce0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.2` + Version = `v11.3.3` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 03d021bd..a4e0aca8 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.3/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.16.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index f83d9294..610e5646 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.3/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.16.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index a0305180..b4d56d11 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.15.3/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.16.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index be41cb83..d315685e 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -155,7 +155,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 15003 + goodEnough := version >= 16000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index bd9bd5d8..2502eb32 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.3.3 (date: 8.10.2021) + +- micromamba update to version 0.16.0 +- minor change on os.Stat usage in holotree functions +- changed minimum required worker count to 2 (was 4 previously) + ## v11.3.2 (date: 7.10.2021) - templates are removed when quick cleanup is requested diff --git a/htfs/functions.go b/htfs/functions.go index 216b1f4d..9d97113a 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -267,9 +267,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat files := make(map[string]bool) for _, part := range content { directpath := filepath.Join(path, part.Name()) - info, err := os.Stat(directpath) - anywork.OnErrPanicCloseAll(err) - if info.IsDir() { + if part.IsDir() { _, ok := it.Dirs[part.Name()] if !ok { common.Trace("* Holotree: remove extra directory %q", directpath) @@ -288,6 +286,8 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat } shadow, ok := current[directpath] golden := !ok || found.Digest == shadow + info, err := part.Info() + anywork.OnErrPanicCloseAll(err) ok = golden && found.Match(info) stats.Dirty(!ok) if !ok { From 69bd7a4a7fe4adbb88c41c7bc1a9f33b8b332e6b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 11 Oct 2021 15:14:13 +0300 Subject: [PATCH 202/516] RCC-202: bug hunting edition (v11.3.4) - new toplevel flag to turn on `--strict` environment handling, and for now this make rcc to run `pip check` after environment install completes - added timeout to metrics sending --- cloud/client.go | 12 ++++++++++++ cloud/metrics.go | 1 + cmd/root.go | 1 + common/logger.go | 4 ++-- common/variables.go | 1 + common/version.go | 2 +- conda/workflows.go | 19 ++++++++++++++++++- docs/changelog.md | 6 ++++++ htfs/commands.go | 8 ++++---- htfs/library.go | 10 +++++++++- mocks/client.go | 5 +++++ robot_tests/export_holozip.robot | 4 ++-- robot_tests/fullrun.robot | 22 +++++++++++----------- 13 files changed, 73 insertions(+), 22 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index a537412b..1bdbbc85 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -47,6 +48,7 @@ type Client interface { Put(request *Request) *Response Delete(request *Request) *Response NewClient(endpoint string) (Client, error) + WithTimeout(time.Duration) Client } func EnsureHttps(endpoint string) (string, error) { @@ -75,6 +77,16 @@ func NewClient(endpoint string) (Client, error) { }, nil } +func (it *internalClient) WithTimeout(timeout time.Duration) Client { + return &internalClient{ + endpoint: it.endpoint, + client: &http.Client{ + Transport: settings.Global.ConfiguredHttpTransport(), + Timeout: timeout, + }, + } +} + func (it *internalClient) NewClient(endpoint string) (Client, error) { return NewClient(endpoint) } diff --git a/cloud/metrics.go b/cloud/metrics.go index 09f9e9e6..418deb5b 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -34,6 +34,7 @@ func sendMetric(metricsHost, kind, name, value string) { common.Debug("ERROR: %v", err) return } + client = client.WithTimeout(5 * time.Second) timestamp := time.Now().UnixNano() url := fmt.Sprintf(trackingUrl, url.PathEscape(kind), timestamp, url.PathEscape(xviper.TrackingIdentity()), url.PathEscape(name), url.PathEscape(value)) common.Debug("Sending metric as %v%v", metricsHost, url) diff --git a/cmd/root.go b/cmd/root.go index cc1ced91..ce0a5bc4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -107,6 +107,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.DebugFlag, "debug", "", false, "to get debug output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TraceFlag, "trace", "", false, "to get trace output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") + rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") } func initConfig() { diff --git a/common/logger.go b/common/logger.go index c6c2c01e..e4840335 100644 --- a/common/logger.go +++ b/common/logger.go @@ -102,6 +102,6 @@ func Progress(step int, form string, details ...interface{}) { ProgressMark = time.Now() delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() message := fmt.Sprintf(form, details...) - Log("#### Progress: %02d/12 %s %8.3fs %s", step, Version, delta, message) - Timeline("%d/12 %s", step, message) + Log("#### Progress: %02d/13 %s %8.3fs %s", step, Version, delta, message) + Timeline("%d/13 %s", step, message) } diff --git a/common/variables.go b/common/variables.go index 60f775b6..a4089d6f 100644 --- a/common/variables.go +++ b/common/variables.go @@ -19,6 +19,7 @@ var ( Silent bool DebugFlag bool TraceFlag bool + StrictFlag bool LogLinenumbers bool NoCache bool NoOutputCapture bool diff --git a/common/version.go b/common/version.go index df0b1ce0..b3ff7801 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.3` + Version = `v11.3.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 4e3858fd..caf60c93 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -216,10 +216,27 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if err != nil { common.Log("%sGolden EE failure: %v%s", pretty.Yellow, err, pretty.Reset) } + fmt.Fprintf(planWriter, "\n--- pip check plan @%ss ---\n\n", stopwatch) + if common.StrictFlag && pipUsed { + common.Progress(9, "Running pip check phase.") + pipCommand := common.NewCommander("pip", "check", "--no-color") + pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") + common.Debug("=== pip check phase ===") + code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + if err != nil || code != 0 { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) + common.Timeline("pip check fail.") + common.Fatal(fmt.Sprintf("Pip check [%d/%x]", code, code), err) + return false, false + } + common.Timeline("pip check done.") + } else { + common.Progress(9, "Pip check skipped.") + } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planWriter.Sync() planWriter.Close() - common.Progress(9, "Update installation plan.") + common.Progress(10, "Update installation plan.") finalplan := filepath.Join(targetFolder, "rcc_plan.log") os.Rename(planfile, finalplan) common.Debug("=== finalize phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 2502eb32..c78f3148 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.3.4 (date: 11.10.2021) + +- new toplevel flag to turn on `--strict` environment handling, and for now + this make rcc to run `pip check` after environment install completes +- added timeout to metrics sending + ## v11.3.3 (date: 8.10.2021) - micromamba update to version 0.16.0 diff --git a/htfs/commands.go b/htfs/commands.go index 2a842cf7..6e59134a 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -18,7 +18,7 @@ import ( func NewEnvironment(condafile, holozip string, restore, force bool) (label string, err error) { defer fail.Around(&err) - defer common.Progress(12, "Fresh holotree done [with %d workers].", anywork.Scale()) + defer common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) common.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) callback := pathlib.LockWaitMessage("Serialized environment creation") @@ -54,11 +54,11 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin path := "" if restore { - common.Progress(11, "Restore space from library [with %d workers].", anywork.Scale()) + common.Progress(12, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) } else { - common.Progress(11, "Restoring space skipped.") + common.Progress(12, "Restoring space skipped.") } return path, nil } @@ -99,7 +99,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e err = conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) - common.Progress(10, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) + common.Progress(11, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } diff --git a/htfs/library.go b/htfs/library.go index c1913d04..8240cf9a 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -288,15 +288,23 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er defer locker.Release() journal.Post("space-used", metafile, "normal holotree with blueprint %s from %s", key, catalog) currentstate := make(map[string]string) + mode := fmt.Sprintf("new space for %q", key) shadow, err := NewRoot(targetdir) if err == nil { err = shadow.LoadFrom(metafile) } if err == nil { - common.TimelineBegin("holotree digest start") + if key == shadow.Blueprint { + mode = fmt.Sprintf("cleaned up space for %q", key) + } else { + mode = fmt.Sprintf("coverted space from %q to %q", shadow.Blueprint, key) + } + common.TimelineBegin("holotree digest start [%q -> %q]", shadow.Blueprint, key) shadow.Treetop(DigestRecorder(currentstate)) common.TimelineEnd() } + common.Timeline("mode: %s", mode) + common.Debug("Holotree operating mode is: %s", mode) err = fs.Relocate(targetdir) fail.On(err != nil, "Failed to relocate %s -> %v", targetdir, err) common.TimelineBegin("holotree make branches start") diff --git a/mocks/client.go b/mocks/client.go index 6c2b35d7..b1431a15 100644 --- a/mocks/client.go +++ b/mocks/client.go @@ -2,6 +2,7 @@ package mocks import ( "testing" + "time" "github.com/robocorp/rcc/cloud" ) @@ -28,6 +29,10 @@ func (it *MockClient) NewClient(endpoint string) (cloud.Client, error) { return it, nil } +func (it *MockClient) WithTimeout(time.Duration) cloud.Client { + return it +} + func (it *MockClient) NewRequest(url string) *cloud.Request { return &cloud.Request{ Url: url, diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 7635c6b0..6b70b386 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -32,8 +32,8 @@ Goal: Create environment for standalone robot Must Have 4e67cd8d4_fcb4b859 Use STDERR Must Have Downloading micromamba - Must Have Progress: 01/12 - Must Have Progress: 12/12 + Must Have Progress: 01/13 + Must Have Progress: 13/13 Goal: Must have author space visible Step build/rcc ht ls diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 1eb0b199..37c419bb 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -77,10 +77,10 @@ Goal: Run task in place in debug mode and with timeline. Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR - Must Have Progress: 01/12 - Must Have Progress: 02/12 - Must Have Progress: 03/12 - Must Have Progress: 12/12 + Must Have Progress: 01/13 + Must Have Progress: 02/13 + Must Have Progress: 03/13 + Must Have Progress: 13/13 Must Have rpaframework Must Have PID # Must Have [N] @@ -112,13 +112,13 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 01/12 - Wont Have Progress: 03/12 - Wont Have Progress: 05/12 - Wont Have Progress: 07/12 - Wont Have Progress: 09/12 - Must Have Progress: 11/12 - Must Have Progress: 12/12 + Must Have Progress: 01/13 + Wont Have Progress: 03/13 + Wont Have Progress: 05/13 + Wont Have Progress: 07/13 + Wont Have Progress: 09/13 + Must Have Progress: 12/13 + Must Have Progress: 13/13 Must Have OK. Goal: Merge two different conda.yaml files with conflict fails From 60b62ce7b152785b9b8a35efde454c86b03e0e18 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 12 Oct 2021 17:03:59 +0300 Subject: [PATCH 203/516] RCC-202: bug hunting edition (v11.3.5) - bugfix: added retries and better error message on holotree rename pattern --- common/version.go | 2 +- docs/changelog.md | 6 +++++- htfs/functions.go | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index b3ff7801..a5a5a847 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.4` + Version = `v11.3.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index c78f3148..01a1c43e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v11.3.4 (date: 11.10.2021) +## v11.3.5 (date: 12.10.2021) + +- bugfix: added retries and better error message on holotree rename pattern + +## v11.3.4 (date: 12.10.2021) - new toplevel flag to turn on `--strict` environment handling, and for now this make rcc to run `pip check` after environment install completes diff --git a/htfs/functions.go b/htfs/functions.go index 9d97113a..0a5824e8 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -5,9 +5,11 @@ import ( "crypto/sha256" "fmt" "io" + "math/rand" "os" "path/filepath" "runtime" + "time" "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" @@ -186,6 +188,32 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { return scheduler } +func TryRename(context, source, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Rename(source, target) + if err == nil { + return nil + } + } + common.Debug("Heads up: rename about to fail [%q -> %q], reason: %s", source, target, err) + origin := "source" + intermediate := fmt.Sprintf("%s.%d_%x", source, os.Getpid(), rand.Intn(4096)) + err = os.Rename(source, intermediate) + if err == nil { + source = intermediate + origin = "target" + } + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Rename(source, target) + if err == nil { + return nil + } + } + return fmt.Errorf("Rename failure [%s/%s/%s/%s], reason: %s", context, common.ControllerType, common.HolotreeSpace, origin, err) +} + func LiftFile(sourcename, sinkname string) anywork.Work { return func() { source, err := os.Open(sourcename) @@ -210,7 +238,7 @@ func LiftFile(sourcename, sinkname string) anywork.Work { runtime.Gosched() - anywork.OnErrPanicCloseAll(os.Rename(partname, sinkname)) + anywork.OnErrPanicCloseAll(TryRename("liftfile", partname, sinkname)) } } @@ -240,7 +268,7 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ anywork.OnErrPanicCloseAll(sink.Close()) - anywork.OnErrPanicCloseAll(os.Rename(partname, sinkname)) + anywork.OnErrPanicCloseAll(TryRename("dropfile", partname, sinkname)) anywork.OnErrPanicCloseAll(os.Chmod(sinkname, details.Mode)) anywork.OnErrPanicCloseAll(os.Chtimes(sinkname, motherTime, motherTime)) From d1ca557a5959c63d01b4937232b2c3110c6cd971 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 13 Oct 2021 14:28:08 +0300 Subject: [PATCH 204/516] RCC-202: bug hunting edition (v11.3.6) - bugfix: added retries to holotree file removal --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 6 +++--- htfs/functions.go | 28 +++++++++++++++++++++++++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index a5a5a847..108e89df 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.5` + Version = `v11.3.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index 01a1c43e..5553a317 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.3.6 (date: 13.10.2021) + +- bugfix: added retries to holotree file removal + ## v11.3.5 (date: 12.10.2021) - bugfix: added retries and better error message on holotree rename pattern diff --git a/htfs/commands.go b/htfs/commands.go index 6e59134a..8f94bc5a 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -66,7 +66,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin func CleanupHolotreeStage(tree MutableLibrary) error { common.Timeline("holotree stage removal start") defer common.Timeline("holotree stage removal done") - return os.RemoveAll(tree.Stage()) + return TryRemoveAll("stage", tree.Stage()) } func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err error) { @@ -131,8 +131,8 @@ func RemoveHolotreeSpace(label string) (err error) { if name != label { continue } - os.Remove(metafile) - err = os.RemoveAll(directory) + TryRemove("metafile", metafile) + err = TryRemoveAll("space", directory) fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) } return nil diff --git a/htfs/functions.go b/htfs/functions.go index 0a5824e8..75b07e17 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -188,6 +188,28 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { return scheduler } +func TryRemove(context, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Remove(target) + if err == nil { + return nil + } + } + return fmt.Errorf("Remove failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) +} + +func TryRemoveAll(context, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.RemoveAll(target) + if err == nil { + return nil + } + } + return fmt.Errorf("RemoveAll failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) +} + func TryRename(context, source, target string) (err error) { for delay := 0; delay < 5; delay += 1 { time.Sleep(time.Duration(delay*100) * time.Millisecond) @@ -211,7 +233,7 @@ func TryRename(context, source, target string) (err error) { return nil } } - return fmt.Errorf("Rename failure [%s/%s/%s/%s], reason: %s", context, common.ControllerType, common.HolotreeSpace, origin, err) + return fmt.Errorf("Rename failure [%s, %s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, origin, err) } func LiftFile(sourcename, sinkname string) anywork.Work { @@ -277,13 +299,13 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ func RemoveFile(filename string) anywork.Work { return func() { - anywork.OnErrPanicCloseAll(os.Remove(filename)) + anywork.OnErrPanicCloseAll(TryRemove("file", filename)) } } func RemoveDirectory(dirname string) anywork.Work { return func() { - anywork.OnErrPanicCloseAll(os.RemoveAll(dirname)) + anywork.OnErrPanicCloseAll(TryRemoveAll("directory", dirname)) } } From ff36c031d936276ca066358d4bc208322d20bf71 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 18 Oct 2021 09:02:30 +0300 Subject: [PATCH 205/516] RCC-203: speed test inside rcc (v11.4.0) - new command `rcc configuration speedtest` which gives abstract score to both network and filesystem speed - some refactoring to enable above functionality --- assets/speedtest.yaml | 7 +++ cmd/cloudPrepare.go | 2 +- cmd/holotreeBootstrap.go | 2 +- cmd/holotreeVariables.go | 4 +- cmd/rcc/main.go | 5 +- cmd/speed.go | 91 +++++++++++++++++++++++++++++++++ common/scorecard.go | 65 +++++++++++++++++++++++ common/variables.go | 52 +++++++++++++------ common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 25 ++------- docs/changelog.md | 6 +++ htfs/commands.go | 12 +++-- htfs/functions.go | 3 +- htfs/library.go | 3 +- operations/issues.go | 5 +- operations/running.go | 2 +- robot/robot.go | 6 +-- 20 files changed, 236 insertions(+), 62 deletions(-) create mode 100644 assets/speedtest.yaml create mode 100644 cmd/speed.go create mode 100644 common/scorecard.go diff --git a/assets/speedtest.yaml b/assets/speedtest.yaml new file mode 100644 index 00000000..397d4624 --- /dev/null +++ b/assets/speedtest.yaml @@ -0,0 +1,7 @@ +channels: +- conda-forge +dependencies: +- python=3.7.5 +- pip=20.1 +- pip: + - robotframework==4.1.2 diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 83708be2..c331b60a 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -53,7 +53,7 @@ var prepareCloudCmd = &cobra.Command{ var label string condafile := config.CondaConfigFile() - label, err = htfs.NewEnvironment(condafile, config.Holozip(), true, false) + label, _, err = htfs.NewEnvironment(condafile, config.Holozip(), true, false) pretty.Guard(err == nil, 8, "Error: %v", err) common.Log("Prepared %q.", label) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 856b15a4..497953da 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -36,7 +36,7 @@ func updateEnvironments(robots []string) { if !config.UsesConda() { continue } - _, err = htfs.NewEnvironment(config.CondaConfigFile(), "", false, false) + _, _, err = htfs.NewEnvironment(config.CondaConfigFile(), "", false, false) pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) } } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index a4742613..f9903e09 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -70,7 +70,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) - condafile := filepath.Join(conda.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) + condafile := filepath.Join(common.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) err = os.WriteFile(condafile, holotreeBlueprint, 0o644) pretty.Guard(err == nil, 6, "%s", err) @@ -78,7 +78,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp if config != nil { holozip = config.Holozip() } - path, err := htfs.NewEnvironment(condafile, holozip, true, force) + path, _, err := htfs.NewEnvironment(condafile, holozip, true, force) pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index ee3b6f50..944a1c0a 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -10,7 +10,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" ) @@ -82,9 +81,9 @@ func markTempForRecycling() { return } markedAlready = true - filename := filepath.Join(conda.RobocorpTemp(), "recycle.now") + filename := filepath.Join(common.RobocorpTemp(), "recycle.now") ioutil.WriteFile(filename, []byte("True"), 0o644) - common.Debug("Marked %q for recycling.", conda.RobocorpTemp()) + common.Debug("Marked %q for recycling.", common.RobocorpTemp()) } func main() { diff --git a/cmd/speed.go b/cmd/speed.go new file mode 100644 index 00000000..e8a5303d --- /dev/null +++ b/cmd/speed.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "fmt" + "math/rand" + "os" + "path/filepath" + "time" + + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + randomIdentifier string +) + +func init() { + randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) +} + +func workingWorm(pipe chan bool) { + fmt.Fprintf(os.Stderr, "\nWorking: -----") + index := 0 +loop: + for { + fmt.Fprintf(os.Stderr, "\b\b\b\b\b%4ds", index) + os.Stderr.Sync() + select { + case <-time.After(1 * time.Second): + index += 1 + continue + case <-pipe: + break loop + } + } +} + +var speedtestCmd = &cobra.Command{ + Use: "speedtest", + Aliases: []string{"speed"}, + Short: "Run system speed test to find how rcc performs in your system.", + Long: "Run system speed test to find how rcc performs in your system.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Speed test run lasted").Report() + } + common.Log("Running network and filesystem performance tests with %d workers.", anywork.Scale()) + common.Log("This may take several minutes, please be patient.") + signal := make(chan bool) + go workingWorm(signal) + silent, trace, debug := common.Silent, common.TraceFlag, common.DebugFlag + common.Silent = true + common.UnifyVerbosityFlags() + folder := common.RobocorpTemp() + content, err := blobs.Asset("assets/speedtest.yaml") + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + condafile := filepath.Join(folder, "speedtest.yaml") + err = os.WriteFile(condafile, content, 0o666) + if err != nil { + pretty.Exit(2, "Error: %v", err) + } + common.ForcedRobocorpHome = folder + _, score, err := htfs.NewEnvironment(condafile, "", true, true) + common.Silent, common.TraceFlag, common.DebugFlag = silent, trace, debug + common.UnifyVerbosityFlags() + if err != nil { + pretty.Exit(3, "Error: %v", err) + } + common.ForcedRobocorpHome = "" + err = os.RemoveAll(folder) + if err != nil { + pretty.Exit(4, "Error: %v", err) + } + score.Done() + close(signal) + common.Log("%s", score.Score()) + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(speedtestCmd) +} diff --git a/common/scorecard.go b/common/scorecard.go new file mode 100644 index 00000000..1e7bbcef --- /dev/null +++ b/common/scorecard.go @@ -0,0 +1,65 @@ +package common + +import ( + "fmt" + "time" +) + +const ( + perfMessage = ` + +Here are performance score results. Higher is better, 0 is reference point. +Network score is %d and filesystem score is %d. +` + topScale = 125 + netScale = 1000 + fsScale = 100 +) + +type Scorecard interface { + Start() Scorecard + Midpoint() Scorecard + Done() Scorecard + Score() string +} + +type scorecard struct { + start time.Time + network time.Time + filesystem time.Time +} + +func (it *scorecard) Score() string { + network := it.network.Sub(it.start).Milliseconds() + filesystem := it.filesystem.Sub(it.network).Milliseconds() + Debug("Raw score values: network=%d and filesystem=%d", network, filesystem) + if network < 1 || filesystem < 0 { + return "Score: N/A [measurement not done]" + } + + return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale)) +} + +func (it *scorecard) Start() Scorecard { + it.start = time.Now() + return it +} + +func (it *scorecard) Midpoint() Scorecard { + it.network = time.Now() + return it +} + +func (it *scorecard) Done() Scorecard { + it.filesystem = time.Now() + return it +} + +func NewScorecard() Scorecard { + marker := time.Now() + return &scorecard{ + start: marker, + network: marker, + filesystem: marker, + } +} diff --git a/common/variables.go b/common/variables.go index a4089d6f..c72e2a8e 100644 --- a/common/variables.go +++ b/common/variables.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "math/rand" "os" "path/filepath" "runtime" @@ -16,22 +17,24 @@ const ( ) var ( - Silent bool - DebugFlag bool - TraceFlag bool - StrictFlag bool - LogLinenumbers bool - NoCache bool - NoOutputCapture bool - Liveonly bool - StageFolder string - ControllerType string - HolotreeSpace string - EnvironmentHash string - SemanticTag string - When int64 - ProgressMark time.Time - Clock *stopwatch + Silent bool + DebugFlag bool + TraceFlag bool + StrictFlag bool + LogLinenumbers bool + NoCache bool + NoOutputCapture bool + Liveonly bool + StageFolder string + ControllerType string + HolotreeSpace string + EnvironmentHash string + SemanticTag string + ForcedRobocorpHome string + When int64 + ProgressMark time.Time + Clock *stopwatch + randomIdentifier string ) func init() { @@ -39,6 +42,7 @@ func init() { When = Clock.started.Unix() ProgressMark = time.Now() + randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) ensureDirectory(TemplateLocation()) ensureDirectory(BinLocation()) ensureDirectory(HolotreeLocation()) @@ -51,6 +55,9 @@ func init() { } func RobocorpHome() string { + if len(ForcedRobocorpHome) > 0 { + return ExpandPath(ForcedRobocorpHome) + } home := os.Getenv(ROBOCORP_HOME_VARIABLE) if len(home) > 0 { return ExpandPath(home) @@ -90,6 +97,19 @@ func RobocorpTempRoot() string { return filepath.Join(RobocorpHome(), "temp") } +func RobocorpTemp() string { + tempLocation := filepath.Join(RobocorpTempRoot(), randomIdentifier) + fullpath, err := filepath.Abs(tempLocation) + if err != nil { + fullpath = tempLocation + } + ensureDirectory(fullpath) + if err != nil { + Log("WARNING (%v) -> %v", tempLocation, err) + } + return fullpath +} + func BinLocation() string { return filepath.Join(RobocorpHome(), "bin") } diff --git a/common/version.go b/common/version.go index 108e89df..b790c734 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.3.6` + Version = `v11.4.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index a4e0aca8..afcdf484 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -29,7 +29,7 @@ var ( func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) - tempFolder := RobocorpTemp() + tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 610e5646..bd50982f 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -33,7 +33,7 @@ func MicromambaLink() string { func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) - tempFolder := RobocorpTemp() + tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index b4d56d11..b1ab6dc5 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -39,7 +39,7 @@ var ( func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) - tempFolder := RobocorpTemp() + tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) return env diff --git a/conda/robocorp.go b/conda/robocorp.go index d315685e..a6d4087c 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -3,7 +3,6 @@ package conda import ( "crypto/sha256" "fmt" - "math/rand" "os" "path/filepath" "regexp" @@ -18,15 +17,10 @@ import ( ) var ( - ignoredPaths = []string{"python", "conda"} - hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") - randomIdentifier string + ignoredPaths = []string{"python", "conda"} + hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") ) -func init() { - randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) -} - func sorted(files []os.FileInfo) { sort.SliceStable(files, func(left, right int) bool { return files[left].Name() < files[right].Name() @@ -101,13 +95,13 @@ func EnvironmentExtensionFor(location string) []string { "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "PYTHONDONTWRITEBYTECODE=x", - "PYTHONPYCACHEPREFIX="+RobocorpTemp(), + "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), - "TEMP="+RobocorpTemp(), - "TMP="+RobocorpTemp(), + "TEMP="+common.RobocorpTemp(), + "TMP="+common.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) @@ -161,15 +155,6 @@ func HasMicroMamba() bool { return goodEnough } -func RobocorpTemp() string { - tempLocation := filepath.Join(common.RobocorpTempRoot(), randomIdentifier) - fullpath, err := pathlib.EnsureDirectory(tempLocation) - if err != nil { - common.Log("WARNING (%v) -> %v", tempLocation, err) - } - return fullpath -} - func LocalChannel() (string, bool) { basefolder := filepath.Join(common.RobocorpHome(), "channel") fullpath := filepath.Join(basefolder, "channeldata.json") diff --git a/docs/changelog.md b/docs/changelog.md index 5553a317..84489adf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.4.0 (date: 18.10.2021) + +- new command `rcc configuration speedtest` which gives abstract score to both + network and filesystem speed +- some refactoring to enable above functionality + ## v11.3.6 (date: 13.10.2021) - bugfix: added retries to holotree file removal diff --git a/htfs/commands.go b/htfs/commands.go index 8f94bc5a..132bf5b0 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -15,7 +15,7 @@ import ( "github.com/robocorp/rcc/xviper" ) -func NewEnvironment(condafile, holozip string, restore, force bool) (label string, err error) { +func NewEnvironment(condafile, holozip string, restore, force bool) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) defer common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) @@ -41,13 +41,15 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin tree = Virtual() common.Timeline("downgraded to virtual holotree library") } + scorecard = common.NewScorecard() var library Library if haszip { library, err = ZipLibrary(holozip) fail.On(err != nil, "Failed to load %q -> %s", holozip, err) common.Timeline("downgraded to holotree zip library") } else { - err = RecordEnvironment(tree, holotreeBlueprint, force) + scorecard.Start() + err = RecordEnvironment(tree, holotreeBlueprint, force, scorecard) fail.On(err != nil, "%s", err) library = tree } @@ -60,7 +62,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin } else { common.Progress(12, "Restoring space skipped.") } - return path, nil + return path, scorecard, nil } func CleanupHolotreeStage(tree MutableLibrary) error { @@ -69,7 +71,7 @@ func CleanupHolotreeStage(tree MutableLibrary) error { return TryRemoveAll("stage", tree.Stage()) } -func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err error) { +func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorecard common.Scorecard) (err error) { defer fail.Around(&err) // following must be setup here @@ -99,6 +101,8 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e err = conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) + scorecard.Midpoint() + common.Progress(11, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) diff --git a/htfs/functions.go b/htfs/functions.go index 75b07e17..a9c198c3 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -13,7 +13,6 @@ import ( "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/trollhash" @@ -437,7 +436,7 @@ func LoadCatalogs() ([]string, []*Root) { func CatalogLoader(catalog string, at int, roots []*Root) anywork.Work { return func() { - tempdir := filepath.Join(conda.RobocorpTemp(), "shadow") + tempdir := filepath.Join(common.RobocorpTemp(), "shadow") shadow, err := NewRoot(tempdir) if err != nil { panic(fmt.Sprintf("Temp dir %q, reason: %v", tempdir, err)) diff --git a/htfs/library.go b/htfs/library.go index 8240cf9a..75f1e1c3 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -15,7 +15,6 @@ import ( "github.com/dchest/siphash" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" @@ -206,7 +205,7 @@ func (it *hololib) queryBlueprint(key string) bool { if !pathlib.IsFile(catalog) { return false } - tempdir := filepath.Join(conda.RobocorpTemp(), key) + tempdir := filepath.Join(common.RobocorpTemp(), key) shadow, err := NewRoot(tempdir) if err != nil { return false diff --git a/operations/issues.go b/operations/issues.go index ce4b53b8..0052c753 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -9,7 +9,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" @@ -33,7 +32,7 @@ func loadToken(reportFile string) (Token, error) { } func createIssueZip(attachmentsFiles []string) (string, error) { - zipfile := filepath.Join(conda.RobocorpTemp(), "attachments.zip") + zipfile := filepath.Join(common.RobocorpTemp(), "attachments.zip") zipper, err := newZipper(zipfile) if err != nil { return "", err @@ -58,7 +57,7 @@ func createIssueZip(attachmentsFiles []string) (string, error) { } func createDiagnosticsReport(robotfile string) (string, *common.DiagnosticStatus, error) { - file := filepath.Join(conda.RobocorpTemp(), "diagnostics.txt") + file := filepath.Join(common.RobocorpTemp(), "diagnostics.txt") diagnostics, err := ProduceDiagnostics(file, robotfile, false, false) if err != nil { return "", nil, err diff --git a/operations/running.go b/operations/running.go index e900c9a5..ab537813 100644 --- a/operations/running.go +++ b/operations/running.go @@ -114,7 +114,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. return true, config, todo, "" } - label, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force) + label, _, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/robot/robot.go b/robot/robot.go index 34a2828c..bf511710 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -429,13 +429,13 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", "PYTHONDONTWRITEBYTECODE=x", - "PYTHONPYCACHEPREFIX="+conda.RobocorpTemp(), + "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), - "TEMP="+conda.RobocorpTemp(), - "TMP="+conda.RobocorpTemp(), + "TEMP="+common.RobocorpTemp(), + "TMP="+common.RobocorpTemp(), searchPath.AsEnvironmental("PATH"), it.PythonPaths().AsEnvironmental("PYTHONPATH"), fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory()), From 253f584bfeb14b5448c9ff756dff3fc764e88a39 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 18 Oct 2021 11:35:57 +0300 Subject: [PATCH 206/516] RCC-203: speed test inside rcc (v11.4.1) - minor textual improvements on abstract score reporting --- common/scorecard.go | 7 +++++-- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/common/scorecard.go b/common/scorecard.go index 1e7bbcef..6e3fb53a 100644 --- a/common/scorecard.go +++ b/common/scorecard.go @@ -3,13 +3,16 @@ package common import ( "fmt" "time" + + "github.com/robocorp/rcc/anywork" ) const ( perfMessage = ` Here are performance score results. Higher is better, 0 is reference point. -Network score is %d and filesystem score is %d. + +Network score is %d and filesystem score is %d. With %d workers on %q. ` topScale = 125 netScale = 1000 @@ -37,7 +40,7 @@ func (it *scorecard) Score() string { return "Score: N/A [measurement not done]" } - return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale)) + return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale), anywork.Scale(), Platform()) } func (it *scorecard) Start() Scorecard { diff --git a/common/version.go b/common/version.go index b790c734..ede793b6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.4.0` + Version = `v11.4.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 84489adf..5b83d1d6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.4.1 (date: 18.10.2021) + +- minor textual improvements on abstract score reporting + ## v11.4.0 (date: 18.10.2021) - new command `rcc configuration speedtest` which gives abstract score to both From 935f7001e547c9f7b36649ab82aee895c1375a14 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 19 Oct 2021 10:39:51 +0300 Subject: [PATCH 207/516] RCC-203: speed test inside rcc (v11.4.2) - one more improvement on abstract score reporting (time is also scored) --- cmd/speed.go | 15 +++++++++------ common/scorecard.go | 8 ++++---- common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cmd/speed.go b/cmd/speed.go index e8a5303d..cd76948a 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -24,21 +24,22 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) } -func workingWorm(pipe chan bool) { +func workingWorm(pipe chan bool, reply chan int) { fmt.Fprintf(os.Stderr, "\nWorking: -----") - index := 0 + seconds := 0 loop: for { - fmt.Fprintf(os.Stderr, "\b\b\b\b\b%4ds", index) + fmt.Fprintf(os.Stderr, "\b\b\b\b\b%4ds", seconds) os.Stderr.Sync() select { case <-time.After(1 * time.Second): - index += 1 + seconds += 1 continue case <-pipe: break loop } } + reply <- seconds } var speedtestCmd = &cobra.Command{ @@ -53,7 +54,8 @@ var speedtestCmd = &cobra.Command{ common.Log("Running network and filesystem performance tests with %d workers.", anywork.Scale()) common.Log("This may take several minutes, please be patient.") signal := make(chan bool) - go workingWorm(signal) + timing := make(chan int) + go workingWorm(signal, timing) silent, trace, debug := common.Silent, common.TraceFlag, common.DebugFlag common.Silent = true common.UnifyVerbosityFlags() @@ -81,7 +83,8 @@ var speedtestCmd = &cobra.Command{ } score.Done() close(signal) - common.Log("%s", score.Score()) + elapsed := <-timing + common.Log("%s", score.Score(elapsed)) pretty.Ok() }, } diff --git a/common/scorecard.go b/common/scorecard.go index 6e3fb53a..37fb9175 100644 --- a/common/scorecard.go +++ b/common/scorecard.go @@ -12,7 +12,7 @@ const ( Here are performance score results. Higher is better, 0 is reference point. -Network score is %d and filesystem score is %d. With %d workers on %q. +Score for network=%d, filesystem=%d, and time=%d with %d workers on %q. ` topScale = 125 netScale = 1000 @@ -23,7 +23,7 @@ type Scorecard interface { Start() Scorecard Midpoint() Scorecard Done() Scorecard - Score() string + Score(int) string } type scorecard struct { @@ -32,7 +32,7 @@ type scorecard struct { filesystem time.Time } -func (it *scorecard) Score() string { +func (it *scorecard) Score(seconds int) string { network := it.network.Sub(it.start).Milliseconds() filesystem := it.filesystem.Sub(it.network).Milliseconds() Debug("Raw score values: network=%d and filesystem=%d", network, filesystem) @@ -40,7 +40,7 @@ func (it *scorecard) Score() string { return "Score: N/A [measurement not done]" } - return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale), anywork.Scale(), Platform()) + return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale), seconds, anywork.Scale(), Platform()) } func (it *scorecard) Start() Scorecard { diff --git a/common/version.go b/common/version.go index ede793b6..c8cae91b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.4.1` + Version = `v11.4.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5b83d1d6..05c6669e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.4.2 (date: 19.10.2021) + +- one more improvement on abstract score reporting (time is also scored) + ## v11.4.1 (date: 18.10.2021) - minor textual improvements on abstract score reporting From 3d73249448abc160f79e56afecded2f18f047f86 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Oct 2021 09:44:30 +0300 Subject: [PATCH 208/516] RCC-204: bug fixing virtual holotree (v11.4.3) - fixing bug where gzipped files in virtual holotree get accidentally expanded when doing `--liveonly` environments - added global `--workers` option to allow control of background worker count --- anywork/worker.go | 14 +++++++++----- cmd/root.go | 3 +++ common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/delegates.go | 4 ++-- htfs/library.go | 2 +- htfs/virtual.go | 2 +- robot_tests/bug_reports.robot | 23 +++++++++++++++++++++++ robot_tests/spellbug/conda.yaml | 7 +++++++ robot_tests/spellbug/robot.yaml | 14 ++++++++++++++ robot_tests/spellbug/task.py | 4 ++++ 11 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 robot_tests/spellbug/conda.yaml create mode 100644 robot_tests/spellbug/robot.yaml create mode 100755 robot_tests/spellbug/task.py diff --git a/anywork/worker.go b/anywork/worker.go index 53918381..c1dda593 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -9,11 +9,12 @@ import ( ) var ( - group *sync.WaitGroup - pipeline WorkQueue - failpipe Failures - errcount Counters - headcount uint64 + group *sync.WaitGroup + pipeline WorkQueue + failpipe Failures + errcount Counters + headcount uint64 + WorkerCount int ) type Work func() @@ -74,6 +75,9 @@ func Scale() uint64 { func AutoScale() { limit := uint64(runtime.NumCPU() - 1) + if WorkerCount > 1 { + limit = uint64(WorkerCount) + } if limit > 96 { limit = 96 } diff --git a/cmd/root.go b/cmd/root.go index ce0a5bc4..f39d6d79 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "runtime/pprof" "strings" + "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" @@ -108,6 +109,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.TraceFlag, "trace", "", false, "to get trace output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") + rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") } func initConfig() { @@ -133,4 +135,5 @@ func initConfig() { common.Trace("CLI command was: %#v", os.Args) common.Debug("Using config file: %v", xviper.ConfigFileUsed()) conda.ValidateLocations() + anywork.AutoScale() } diff --git a/common/version.go b/common/version.go index c8cae91b..e2c6a6dc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.4.2` + Version = `v11.4.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 05c6669e..49efdf25 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.4.3 (date: 20.10.2021) + +- fixing bug where gzipped files in virtual holotree get accidentally + expanded when doing `--liveonly` environments +- added global `--workers` option to allow control of background worker count + ## v11.4.2 (date: 19.10.2021) - one more improvement on abstract score reporting (time is also scored) diff --git a/htfs/delegates.go b/htfs/delegates.go index b31a0f46..3a6eb3f8 100644 --- a/htfs/delegates.go +++ b/htfs/delegates.go @@ -8,7 +8,7 @@ import ( "github.com/robocorp/rcc/fail" ) -func delegateOpen(it MutableLibrary, digest string) (readable io.Reader, closer Closer, err error) { +func delegateOpen(it MutableLibrary, digest string, ungzip bool) (readable io.Reader, closer Closer, err error) { defer fail.Around(&err) filename := it.ExactLocation(digest) @@ -17,7 +17,7 @@ func delegateOpen(it MutableLibrary, digest string) (readable io.Reader, closer var reader io.ReadCloser reader, err = gzip.NewReader(source) - if err != nil { + if err != nil || !ungzip { _, err = source.Seek(0, 0) fail.On(err != nil, "Failed to seek %q -> %v", filename, err) reader = source diff --git a/htfs/library.go b/htfs/library.go index 75f1e1c3..42ad10e5 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -71,7 +71,7 @@ type hololib struct { } func (it *hololib) Open(digest string) (readable io.Reader, closer Closer, err error) { - return delegateOpen(it, digest) + return delegateOpen(it, digest, true) } func (it *hololib) Location(digest string) string { diff --git a/htfs/virtual.go b/htfs/virtual.go index 17b91cf1..b9df7f36 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -115,7 +115,7 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { } func (it *virtual) Open(digest string) (readable io.Reader, closer Closer, err error) { - return delegateOpen(it, digest) + return delegateOpen(it, digest, false) } func (it *virtual) ExactLocation(key string) string { diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index c24cdc95..10fe50f8 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -10,6 +10,29 @@ Github issue 7 about initial call with do-not-track Step build/rcc configure identity --controller citests --do-not-track --config tmp/bug_7.yaml Must Have anonymous health tracking is: disabled +Bug in virtual holotree with gzipped files + [Tags] WIP + Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml + Use STDERR + Must Have Blueprint "ef0163b57ff44cd5" is available: false + + Step build/rcc run --liveonly --controller citests --robot robot_tests/spellbug/robot.yaml + Use STDOUT + Must Have Bug fixed! + + Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml + Use STDERR + Must Have Blueprint "ef0163b57ff44cd5" is available: false + + Step build/rcc run --controller citests --robot robot_tests/spellbug/robot.yaml + Use STDOUT + Must Have Bug fixed! + + Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml + Use STDERR + Must Have Blueprint "ef0163b57ff44cd5" is available: true + + *** Keywords *** Remove Config diff --git a/robot_tests/spellbug/conda.yaml b/robot_tests/spellbug/conda.yaml new file mode 100644 index 00000000..2132ae45 --- /dev/null +++ b/robot_tests/spellbug/conda.yaml @@ -0,0 +1,7 @@ +channels: +- conda-forge +dependencies: +- python=3.7.5 +- pip=20.1 +- pip: + - pyspellchecker==0.6.2 diff --git a/robot_tests/spellbug/robot.yaml b/robot_tests/spellbug/robot.yaml new file mode 100644 index 00000000..57971295 --- /dev/null +++ b/robot_tests/spellbug/robot.yaml @@ -0,0 +1,14 @@ +tasks: + entrypoint: + command: + - python + - task.py + +condaConfigFile: conda.yaml +artifactsDir: output +PATH: + - . +PYTHONPATH: + - . +ignoreFiles: + - .gitignore diff --git a/robot_tests/spellbug/task.py b/robot_tests/spellbug/task.py new file mode 100755 index 00000000..4429e3db --- /dev/null +++ b/robot_tests/spellbug/task.py @@ -0,0 +1,4 @@ +from spellchecker import SpellChecker + +SpellChecker() +print("Bug fixed!") From 989bc254a0b98a48182c162cbe6d2a9d87f09fd2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Oct 2021 11:55:21 +0300 Subject: [PATCH 209/516] RCC-205: holotree import command (v11.5.0) - adding initial support for importing hololib.zips into local hololib catalog --- cmd/holotreeImport.go | 29 +++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 cmd/holotreeImport.go diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go new file mode 100644 index 00000000..6c9aa0d5 --- /dev/null +++ b/cmd/holotreeImport.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var holotreeImportCmd = &cobra.Command{ + Use: "import hololib.zip+", + Short: "Import one or more hololib.zip files into local hololib.", + Long: "Import one or more hololib.zip files into local hololib.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree import command lasted").Report() + } + for _, filename := range args { + err := operations.Unzip(common.HololibLocation(), filename, true, false) + pretty.Guard(err == nil, 1, "Could not import %q, reason: %v", filename, err) + } + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeImportCmd) +} diff --git a/common/version.go b/common/version.go index e2c6a6dc..33c13865 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.4.3` + Version = `v11.5.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 49efdf25..7ebc37a8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.5.0 (date: 20.10.2021) + +- adding initial support for importing hololib.zips into local hololib catalog + ## v11.4.3 (date: 20.10.2021) - fixing bug where gzipped files in virtual holotree get accidentally From 65f470ed585fa7ab64242cbb8c885951a5fc3fbd Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 26 Oct 2021 17:33:20 +0300 Subject: [PATCH 210/516] RCC-205: holotree import command (v11.5.1) - adding holotree catalogs command to list available catalogs with more detail - extending holotree list command to show all spaces reachable from hololib catalogs including imported holotree spaces - holotree delete should now also remove space elsewhere (based on imported catalogs and their holotree locations) --- cmd/holotreeCatalogs.go | 46 +++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 8 +++++++ htfs/directory.go | 9 ++++++++ htfs/functions.go | 46 +++++++++++++++++++++++++++++++++++++++++ htfs/library.go | 9 ++++---- 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 cmd/holotreeCatalogs.go diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go new file mode 100644 index 00000000..f4b913ea --- /dev/null +++ b/cmd/holotreeCatalogs.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +const mega = 1024 * 1024 + +func listCatalogDetails() { + tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) + tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside ROBOCORP_HOME)\tHolotree path\n")) + tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t-------------------------------------------------\t-------------\n")) + _, roots := htfs.LoadCatalogs() + for _, catalog := range roots { + stats, err := catalog.Stats() + pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) + megas := stats.Bytes / mega + data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas, stats.Identity, catalog.HolotreeBase()) + tabbed.Write([]byte(data)) + } + tabbed.Flush() +} + +var holotreeCatalogsCmd = &cobra.Command{ + Use: "catalogs", + Short: "List native and imported holotree catalogs.", + Long: "List native and imported holotree catalogs.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree catalogs command lasted").Report() + } + listCatalogDetails() + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeCatalogsCmd) +} diff --git a/common/version.go b/common/version.go index 33c13865..ea3e6577 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.0` + Version = `v11.5.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7ebc37a8..94fa23e0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.5.1 (date: 26.10.2021) + +- adding holotree catalogs command to list available catalogs with more detail +- extending holotree list command to show all spaces reachable from hololib + catalogs including imported holotree spaces +- holotree delete should now also remove space elsewhere (based on imported + catalogs and their holotree locations) + ## v11.5.0 (date: 20.10.2021) - adding initial support for importing hololib.zips into local hololib catalog diff --git a/htfs/directory.go b/htfs/directory.go index 9aa86b14..e254cc6b 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -107,6 +107,15 @@ func (it *Root) Treetop(task Treetop) error { return anywork.Sync() } +func (it *Root) Stats() (*TreeStats, error) { + task, stats := CalculateTreeStats() + err := it.AllDirs(task) + if err != nil { + return nil, err + } + return stats, nil +} + func (it *Root) AllDirs(task Dirtask) error { common.TimelineBegin("holotree dirs sync start") defer common.TimelineEnd() diff --git a/htfs/functions.go b/htfs/functions.go index a9c198c3..abcb66b3 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" "runtime" + "sort" + "sync" "time" "github.com/robocorp/rcc/anywork" @@ -308,6 +310,36 @@ func RemoveDirectory(dirname string) anywork.Work { } } +type TreeStats struct { + sync.Mutex + Directories uint64 + Files uint64 + Bytes uint64 + Identity string +} + +func guessLocation(digest string) string { + return filepath.Join("hololib", "library", digest[:2], digest[2:4], digest[4:6], digest) +} + +func CalculateTreeStats() (Dirtask, *TreeStats) { + result := &TreeStats{} + return func(path string, it *Dir) anywork.Work { + return func() { + result.Lock() + defer result.Unlock() + result.Directories += 1 + result.Files += uint64(len(it.Files)) + for _, file := range it.Files { + result.Bytes += uint64(file.Size) + if file.Name == "identity.yaml" { + result.Identity = guessLocation(file.Digest) + } + } + } + }, result +} + func RestoreDirectory(library Library, fs *Root, current map[string]string, stats *stats) Dirtask { return func(path string, it *Dir) anywork.Work { return func() { @@ -434,6 +466,20 @@ func LoadCatalogs() ([]string, []*Root) { return catalogs, roots } +func BaseFolders() []string { + _, roots := LoadCatalogs() + folders := make(map[string]bool) + result := []string{} + for _, root := range roots { + folders[filepath.Dir(root.Path)] = true + } + for folder, _ := range folders { + result = append(result, folder) + } + sort.Strings(result) + return result +} + func CatalogLoader(catalog string, at int, roots []*Root) anywork.Work { return func() { tempdir := filepath.Join(common.RobocorpTemp(), "shadow") diff --git a/htfs/library.go b/htfs/library.go index 42ad10e5..87429e7e 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -237,10 +237,11 @@ func Catalogs() []string { func Spacemap() map[string]string { result := make(map[string]string) - basedir := common.HolotreeLocation() - for _, metafile := range pathlib.Glob(basedir, "*.meta") { - fullpath := filepath.Join(basedir, metafile) - result[fullpath[:len(fullpath)-5]] = fullpath + for _, basedir := range BaseFolders() { + for _, metafile := range pathlib.Glob(basedir, "*.meta") { + fullpath := filepath.Join(basedir, metafile) + result[fullpath[:len(fullpath)-5]] = fullpath + } } return result } From 979df3123c16f88c52f9563a27e97f39a1afbee6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 27 Oct 2021 08:34:25 +0300 Subject: [PATCH 211/516] RCC-205: holotree import command (v11.5.2) - added `--json` option and output to catalogs listing - bug fix: added missing file detection to holotree check --- cmd/holotreeCatalogs.go | 40 +++++++++++++++++++++++++++++++++++----- cmd/holotreeCheck.go | 12 ++++++++---- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/functions.go | 15 ++++++++++++--- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index f4b913ea..39cd46cb 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -1,8 +1,10 @@ package cmd import ( + "encoding/json" "fmt" "os" + "path/filepath" "text/tabwriter" "github.com/robocorp/rcc/common" @@ -13,16 +15,38 @@ import ( const mega = 1024 * 1024 -func listCatalogDetails() { +func megas(bytes uint64) uint64 { + return bytes / mega +} + +func jsonCatalogDetails(roots []*htfs.Root) { + holder := make(map[string]map[string]interface{}) + for _, catalog := range roots { + stats, err := catalog.Stats() + pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) + data := make(map[string]interface{}) + data["blueprint"] = catalog.Blueprint + data["holotree"] = catalog.HolotreeBase() + data["identity.yaml"] = filepath.Join(common.RobocorpHome(), stats.Identity) + data["platform"] = catalog.Platform + data["directories"] = stats.Directories + data["files"] = stats.Files + data["bytes"] = stats.Bytes + holder[catalog.Blueprint] = data + } + nice, err := json.MarshalIndent(holder, "", " ") + pretty.Guard(err == nil, 2, "%s", err) + common.Stdout("%s\n", nice) +} + +func listCatalogDetails(roots []*htfs.Root) { tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside ROBOCORP_HOME)\tHolotree path\n")) tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t-------------------------------------------------\t-------------\n")) - _, roots := htfs.LoadCatalogs() for _, catalog := range roots { stats, err := catalog.Stats() pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) - megas := stats.Bytes / mega - data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas, stats.Identity, catalog.HolotreeBase()) + data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase()) tabbed.Write([]byte(data)) } tabbed.Flush() @@ -36,11 +60,17 @@ var holotreeCatalogsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree catalogs command lasted").Report() } - listCatalogDetails() + _, roots := htfs.LoadCatalogs() + if jsonFlag { + jsonCatalogDetails(roots) + } else { + listCatalogDetails(roots) + } pretty.Ok() }, } func init() { holotreeCmd.AddCommand(holotreeCatalogsCmd) + holotreeCatalogsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") } diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index 14e4a119..f2d9b6dd 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -20,17 +20,16 @@ func checkHolotreeIntegrity() { err = fs.Lift() pretty.Guard(err == nil, 2, "%s", err) common.Timeline("holotree integrity hasher") - known := htfs.LoadHololibHashes() + known, needed := htfs.LoadHololibHashes() err = fs.AllFiles(htfs.Hasher(known)) pretty.Guard(err == nil, 3, "%s", err) collector := make(map[string]string) common.Timeline("holotree integrity collector") - err = fs.Treetop(htfs.IntegrityCheck(collector)) + err = fs.Treetop(htfs.IntegrityCheck(collector, needed)) common.Timeline("holotree integrity report") pretty.Guard(err == nil, 4, "%s", err) purge := make(map[string]bool) - for k, v := range collector { - fmt.Println(k, v) + for k, _ := range collector { found, ok := known[filepath.Base(k)] if !ok { continue @@ -39,6 +38,11 @@ func checkHolotreeIntegrity() { purge[catalog] = true } } + for _, v := range needed { + for catalog, _ := range v { + purge[catalog] = true + } + } redo := false for k, _ := range purge { fmt.Println("Purge catalog:", k) diff --git a/common/version.go b/common/version.go index ea3e6577..0cc86a63 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.1` + Version = `v11.5.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 94fa23e0..e26beb8c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.5.2 (date: 27.10.2021) + +- added `--json` option and output to catalogs listing +- bug fix: added missing file detection to holotree check + ## v11.5.1 (date: 26.10.2021) - adding holotree catalogs command to list available catalogs with more detail diff --git a/htfs/functions.go b/htfs/functions.go index abcb66b3..11f29673 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -75,7 +75,7 @@ func DigestRecorder(target map[string]string) Treetop { return tool } -func IntegrityCheck(result map[string]string) Treetop { +func IntegrityCheck(result map[string]string, needed map[string]map[string]bool) Treetop { var tool Treetop tool = func(path string, it *Dir) error { for name, subdir := range it.Dirs { @@ -84,6 +84,8 @@ func IntegrityCheck(result map[string]string) Treetop { for name, file := range it.Files { if file.Name != file.Digest { result[filepath.Join(path, name)] = file.Digest + } else { + delete(needed, file.Digest) } } return nil @@ -415,18 +417,25 @@ func ZipRoot(library MutableLibrary, fs *Root, sink Zipper) Treetop { return tool } -func LoadHololibHashes() map[string]map[string]bool { +func LoadHololibHashes() (map[string]map[string]bool, map[string]map[string]bool) { catalogs, roots := LoadCatalogs() slots := make([]map[string]string, len(roots)) for at, root := range roots { anywork.Backlog(DigestLoader(root, at, slots)) } result := make(map[string]map[string]bool) + needed := make(map[string]map[string]bool) runtime.Gosched() anywork.Sync() for at, slot := range slots { catalog := catalogs[at] for k, _ := range slot { + who, ok := needed[k] + if !ok { + who = make(map[string]bool) + needed[k] = who + } + who[catalog] = true found, ok := result[k] if !ok { found = make(map[string]bool) @@ -435,7 +444,7 @@ func LoadHololibHashes() map[string]map[string]bool { found[catalog] = true } } - return result + return result, needed } func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { From f8d58e57c328a59ea3662d09c660d6a738e2eec9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 28 Oct 2021 15:49:40 +0300 Subject: [PATCH 212/516] BUGFIX: robot wrap path fix (v11.5.3) - bugfix: path handling in robot wrap commands --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/zipper.go | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 0cc86a63..3c94935e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.2` + Version = `v11.5.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index e26beb8c..a3633b1c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.5.3 (date: 28.10.2021) + +- bugfix: path handling in robot wrap commands + ## v11.5.2 (date: 27.10.2021) - added `--json` option and output to catalogs listing diff --git a/operations/zipper.go b/operations/zipper.go index eec29aeb..48cb01f1 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -113,7 +113,7 @@ func (it *unzipper) Explode(workers int, directory string) error { } todo <- &WriteTarget{ Source: entry, - Target: filepath.Join(directory, entry.Name), + Target: filepath.Join(directory, filepath.ToSlash(entry.Name)), } } @@ -156,7 +156,7 @@ func (it *unzipper) Extract(directory string) error { if entry.FileInfo().IsDir() { continue } - target := filepath.Join(directory, entry.Name) + target := filepath.Join(directory, filepath.ToSlash(entry.Name)) todo := WriteTarget{ Source: entry, Target: target, @@ -206,7 +206,7 @@ func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { return } defer source.Close() - target, err := it.writer.Create(relativepath) + target, err := it.writer.Create(filepath.ToSlash(relativepath)) if err != nil { it.Note(err) return @@ -218,7 +218,7 @@ func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { } func (it *zipper) AddBlob(relativepath string, blob []byte) { - target, err := it.writer.Create(relativepath) + target, err := it.writer.Create(filepath.ToSlash(relativepath)) if err != nil { it.Note(err) return From a855910a863b1ec9163fe9e5f08a9589f725f747 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 29 Oct 2021 09:41:58 +0300 Subject: [PATCH 213/516] BUGFIX: robot unwrap path fix again (v11.5.4) - bugfix: path handling in robot wrap commands (now cross-platform) --- common/version.go | 2 +- docs/changelog.md | 6 +++++- operations/zipper.go | 18 ++++++++++++++---- operations/zipper_test.go | 20 ++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 operations/zipper_test.go diff --git a/common/version.go b/common/version.go index 3c94935e..cf86647f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.3` + Version = `v11.5.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index a3633b1c..cabe7589 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v11.5.3 (date: 28.10.2021) +## v11.5.4 (date: 29.10.2021) + +- bugfix: path handling in robot wrap commands (now cross-platform) + +## v11.5.3 (date: 28.10.2021) broken - bugfix: path handling in robot wrap commands diff --git a/operations/zipper.go b/operations/zipper.go index 48cb01f1..797cdc53 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -13,6 +14,15 @@ import ( "github.com/robocorp/rcc/robot" ) +const ( + backslash = `\` + slash = `/` +) + +func slashed(text string) string { + return strings.Replace(text, backslash, slash, -1) +} + type WriteTarget struct { Source *zip.File Target string @@ -113,7 +123,7 @@ func (it *unzipper) Explode(workers int, directory string) error { } todo <- &WriteTarget{ Source: entry, - Target: filepath.Join(directory, filepath.ToSlash(entry.Name)), + Target: filepath.Join(directory, slashed(entry.Name)), } } @@ -156,7 +166,7 @@ func (it *unzipper) Extract(directory string) error { if entry.FileInfo().IsDir() { continue } - target := filepath.Join(directory, filepath.ToSlash(entry.Name)) + target := filepath.Join(directory, slashed(entry.Name)) todo := WriteTarget{ Source: entry, Target: target, @@ -206,7 +216,7 @@ func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { return } defer source.Close() - target, err := it.writer.Create(filepath.ToSlash(relativepath)) + target, err := it.writer.Create(slashed(relativepath)) if err != nil { it.Note(err) return @@ -218,7 +228,7 @@ func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { } func (it *zipper) AddBlob(relativepath string, blob []byte) { - target, err := it.writer.Create(filepath.ToSlash(relativepath)) + target, err := it.writer.Create(slashed(relativepath)) if err != nil { it.Note(err) return diff --git a/operations/zipper_test.go b/operations/zipper_test.go new file mode 100644 index 00000000..0a28c8ab --- /dev/null +++ b/operations/zipper_test.go @@ -0,0 +1,20 @@ +package operations + +import ( + "testing" + + "github.com/robocorp/rcc/hamlet" +) + +const ( + wintestpath = `a\b` + nixtestpath = `a/b` +) + +func TestCanConvertSlashes(t *testing.T) { + must, wont := hamlet.Specifications(t) + + wont.Equal(wintestpath, nixtestpath) + must.Equal(3, len(wintestpath)) + must.Equal(slashed(wintestpath), nixtestpath) +} From f458120959536d4352dc06baa1385bd11bc78cfb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 2 Nov 2021 09:45:59 +0200 Subject: [PATCH 214/516] BUGFIX: artifacts directory fix (v11.5.5) - bugfix: robot task format ignored artifacts directory, but now it uses it --- common/version.go | 2 +- docs/changelog.md | 4 ++++ robot/robot.go | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index cf86647f..4b8a64b0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.4` + Version = `v11.5.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index cabe7589..75ed2b20 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.5.5 (date: 2.11.2021) + +- bugfix: robot task format ignored artifacts directory, but now it uses it + ## v11.5.4 (date: 29.10.2021) - bugfix: path handling in robot wrap commands (now cross-platform) diff --git a/robot/robot.go b/robot/robot.go index bf511710..d25cb6fd 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -67,6 +67,15 @@ type task struct { Task string `yaml:"robotTaskName,omitempty"` Shell string `yaml:"shell,omitempty"` Command []string `yaml:"command,omitempty"` + robot *robot +} + +func (it *robot) relink() { + for _, task := range it.Tasks { + if task != nil { + task.robot = it + } + } } func (it *robot) diagnoseTasks(diagnose common.Diagnoser) { @@ -455,11 +464,15 @@ func (it *task) shellCommand() []string { } func (it *task) taskCommand() []string { + output := "output" + if it.robot != nil { + output = it.robot.Artifacts + } return []string{ "python", "-m", "robot", "--report", "NONE", - "--outputdir", "output", + "--outputdir", output, "--logtitle", "Task log", "--task", it.Task, ".", @@ -482,6 +495,7 @@ func robotFrom(content []byte) (*robot, error) { if err != nil { return nil, err } + config.relink() return &config, nil } From 8e29882ef21187a8d1d9a376f6d0470bd2bd4e90 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Dec 2021 12:34:59 +0200 Subject: [PATCH 215/516] UPGRADE: next version of micromamba (v11.6.0) - micromamba update to version 0.19.0 - now `artifactsDir` is explicitely created before robot execution --- README.md | 2 ++ common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 14 ++++++++++---- 8 files changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8b70591d..f64da4aa 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) co

+RCC is actively maintained by [Robocorp](https://www.robocorp.com/). + ## Why use rcc? diff --git a/common/version.go b/common/version.go index 4b8a64b0..5fa9d6ab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.5.5` + Version = `v11.6.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index afcdf484..59c99625 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.16.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.19.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index bd50982f..c8c8de14 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.16.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.19.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index b1ab6dc5..66a8607a 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.16.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.19.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index a6d4087c..5a4f5920 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -149,7 +149,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 16000 + goodEnough := version >= 19000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index 75ed2b20..0df78bec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.6.0 (date: 7.12.2021) + +- micromamba update to version 0.19.0 +- now `artifactsDir` is explicitely created before robot execution + ## v11.5.5 (date: 2.11.2021) - bugfix: robot task format ignored artifacts directory, but now it uses it diff --git a/operations/running.go b/operations/running.go index ab537813..12148ba4 100644 --- a/operations/running.go +++ b/operations/running.go @@ -173,7 +173,10 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t environment = append(environment, fmt.Sprintf("%s=%s", key, value)) } } - outputDir := config.ArtifactDirectory() + outputDir, err := pathlib.EnsureDirectory(config.ArtifactDirectory()) + if err != nil { + pretty.Exit(9, "Error: %v", err) + } common.Debug("about to run command - %v", task) if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) @@ -181,7 +184,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } if err != nil { - pretty.Exit(9, "Error: %v", err) + pretty.Exit(10, "Error: %v", err) } pretty.Ok() } @@ -232,7 +235,10 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } before := make(map[string]string) beforeHash, beforeErr := conda.DigestFor(label, before) - outputDir := config.ArtifactDirectory() + outputDir, err := pathlib.EnsureDirectory(config.ArtifactDirectory()) + if err != nil { + pretty.Exit(9, "Error: %v", err) + } if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { wantedfile, _ := config.DependenciesFile() ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) @@ -248,7 +254,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro afterHash, afterErr := conda.DigestFor(label, after) conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after, true) if err != nil { - pretty.Exit(9, "Error: %v", err) + pretty.Exit(10, "Error: %v", err) } pretty.Ok() } From fc87bdc7e30af788c77295f938cd74b59783e196 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 7 Jan 2022 09:31:27 +0200 Subject: [PATCH 216/516] BUGFIX: micromamba version parsing (v11.6.1) - fixing micromamba version number parsing --- common/version.go | 2 +- conda/robocorp.go | 28 +++++++++++++++++----------- conda/robocorp_test.go | 22 ++++++++++++++++++++++ docs/changelog.md | 6 +++++- 4 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 conda/robocorp_test.go diff --git a/common/version.go b/common/version.go index 5fa9d6ab..fdde159f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.0` + Version = `v11.6.1` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 5a4f5920..5f98e4d1 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -17,8 +17,9 @@ import ( ) var ( - ignoredPaths = []string{"python", "conda"} - hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") + ignoredPaths = []string{"python", "conda"} + hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") + versionPattern = regexp.MustCompile("^[^0-9.]*([0-9.]+)\\s*$") ) func sorted(files []os.FileInfo) { @@ -112,13 +113,18 @@ func EnvironmentFor(location string) []string { return append(os.Environ(), EnvironmentExtensionFor(location)...) } -func asVersion(text string) (uint64, string) { - text = strings.TrimSpace(text) - multiline := strings.SplitN(text, "\n", 2) - if len(multiline) > 0 { - text = strings.TrimSpace(multiline[0]) +func AsVersion(incoming string) (uint64, string) { + incoming = strings.TrimSpace(incoming) + versionText := "0" +search: + for _, line := range strings.SplitN(incoming, "\n", -1) { + found := versionPattern.FindStringSubmatch(line) + if found != nil { + versionText = found[1] + break search + } } - parts := strings.SplitN(text, ".", 4) + parts := strings.SplitN(versionText, ".", 4) steps := len(parts) multipliers := []uint64{1000000, 1000, 1} version := uint64(0) @@ -132,7 +138,7 @@ func asVersion(text string) (uint64, string) { } version += multiplier * value } - return version, text + return version, versionText } func MicromambaVersion() string { @@ -140,7 +146,7 @@ func MicromambaVersion() string { if err != nil { return err.Error() } - _, versionText = asVersion(versionText) + _, versionText = AsVersion(versionText) return versionText } @@ -148,7 +154,7 @@ func HasMicroMamba() bool { if !pathlib.IsFile(BinMicromamba()) { return false } - version, versionText := asVersion(MicromambaVersion()) + version, versionText := AsVersion(MicromambaVersion()) goodEnough := version >= 19000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) diff --git a/conda/robocorp_test.go b/conda/robocorp_test.go new file mode 100644 index 00000000..e830fe22 --- /dev/null +++ b/conda/robocorp_test.go @@ -0,0 +1,22 @@ +package conda_test + +import ( + "testing" + + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/hamlet" +) + +func second(_ interface{}, version string) string { + return version +} + +func TestCanParseMicromambaVersion(t *testing.T) { + must_be, _ := hamlet.Specifications(t) + + must_be.Equal("0", second(conda.AsVersion("python"))) + must_be.Equal("0.19.1", second(conda.AsVersion("0.19.1"))) + must_be.Equal("0.19.0", second(conda.AsVersion("micromamba: 0.19.0"))) + must_be.Equal("0.19.0", second(conda.AsVersion("\n\n\tmicromamba: 0.19.0 \nlibmamba: 0.18.7\n\n\t"))) + must_be.Equal("0.20", second(conda.AsVersion("microrumba: 0.20"))) +} diff --git a/docs/changelog.md b/docs/changelog.md index 0df78bec..e55c1bb3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v11.6.0 (date: 7.12.2021) +## v11.6.1 (date: 7.1.2022) + +- fixing micromamba version number parsing + +## v11.6.0 (date: 7.12.2021) broken - micromamba update to version 0.19.0 - now `artifactsDir` is explicitely created before robot execution From 01b7346e86e7383166e480546d5533aad3c18de8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 7 Jan 2022 14:59:07 +0200 Subject: [PATCH 217/516] BUGFIX: removed some virtual envs from PATH (v11.6.2) - added "pyenv" and "venv" to patterns removed from PATH, since they can break isolation of our environments --- common/version.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index fdde159f..9e27ef5b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.1` + Version = `v11.6.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 5f98e4d1..44343fb7 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -17,7 +17,7 @@ import ( ) var ( - ignoredPaths = []string{"python", "conda"} + ignoredPaths = []string{"python", "conda", "pyenv", "venv"} hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") versionPattern = regexp.MustCompile("^[^0-9.]*([0-9.]+)\\s*$") ) diff --git a/docs/changelog.md b/docs/changelog.md index e55c1bb3..5b8d62b8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.6.2 (date: 7.1.2022) + +- added "pyenv" and "venv" to patterns removed from PATH, since they can + break isolation of our environments + ## v11.6.1 (date: 7.1.2022) - fixing micromamba version number parsing From 9581710c199b470df261c0d6436535bde168382d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 10 Jan 2022 15:28:55 +0200 Subject: [PATCH 218/516] BUGFIX: removed more virtual envs from PATH (v11.6.3) - more patterns added ("pypoetry" and "virtualenv") to be removed from PATH, since they also can break isolation of our environments --- common/version.go | 2 +- conda/robocorp.go | 9 ++++++++- docs/changelog.md | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 9e27ef5b..1165373d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.2` + Version = `v11.6.3` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 44343fb7..69cfd7db 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -17,7 +17,14 @@ import ( ) var ( - ignoredPaths = []string{"python", "conda", "pyenv", "venv"} + ignoredPaths = []string{ + "python", + "conda", + "pyenv", + "venv", + "pypoetry", + "virtualenv", + } hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") versionPattern = regexp.MustCompile("^[^0-9.]*([0-9.]+)\\s*$") ) diff --git a/docs/changelog.md b/docs/changelog.md index 5b8d62b8..697cdb69 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.6.3 (date: 10.1.2022) + +- more patterns added ("pypoetry" and "virtualenv") to be removed from PATH, + since they also can break isolation of our environments + ## v11.6.2 (date: 7.1.2022) - added "pyenv" and "venv" to patterns removed from PATH, since they can From 76230d66d06045e5c2633a0aa4de8c739d63caf6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 23 Feb 2022 11:03:46 +0200 Subject: [PATCH 219/516] BUGFIX: GH#27 wrong PYTHON_EXE path (v11.6.4) - GH#27 fixing issue where rcc finds executables outside of holotree environment. - this closes #27 --- common/version.go | 2 +- conda/robocorp.go | 8 ++++++-- conda/workflows.go | 3 +-- docs/changelog.md | 6 ++++++ robot_tests/holotree.robot | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 1165373d..a1c86565 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.3` + Version = `v11.6.4` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 69cfd7db..2b68487d 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -76,6 +76,10 @@ func DigestFor(folder string, collect map[string]string) ([]byte, error) { return result, nil } +func HolotreePath(environment string) pathlib.PathParts { + return pathlib.PathFrom(CondaPaths(environment)...) +} + func FindPath(environment string) pathlib.PathParts { target := pathlib.TargetPath() target = target.Remove(ignoredPaths) @@ -85,7 +89,7 @@ func FindPath(environment string) pathlib.PathParts { func EnvironmentExtensionFor(location string) []string { environment := make([]string, 0, 20) - searchPath := FindPath(location) + searchPath := HolotreePath(location) python, ok := searchPath.Which("python3", FileExtensions) if !ok { python, ok = searchPath.Which("python", FileExtensions) @@ -110,7 +114,7 @@ func EnvironmentExtensionFor(location string) []string { "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), "TEMP="+common.RobocorpTemp(), "TMP="+common.RobocorpTemp(), - searchPath.AsEnvironmental("PATH"), + FindPath(location).AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) return environment diff --git a/conda/workflows.go b/conda/workflows.go index caf60c93..d900c086 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -26,9 +26,8 @@ func metafile(folder string) string { } func livePrepare(liveFolder string, command ...string) (*shell.Task, error) { - searchPath := FindPath(liveFolder) commandName := command[0] - task, ok := searchPath.Which(commandName, FileExtensions) + task, ok := HolotreePath(liveFolder).Which(commandName, FileExtensions) if !ok { return nil, fmt.Errorf("Cannot find command: %v", commandName) } diff --git a/docs/changelog.md b/docs/changelog.md index 697cdb69..5688a6ea 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.6.4 (date: 23.2.2022) + +- GH#27 fixing issue where rcc finds executables outside of holotree + environment. +- this closes #27 + ## v11.6.3 (date: 10.1.2022) - more patterns added ("pypoetry" and "virtualenv") to be removed from PATH, diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index e972cf10..5829dce1 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -93,7 +93,6 @@ Goal: See variables from specific environment with robot.yaml knowledge in JSON Goal: Liveonly works and uses virtual holotree Step build/rcc holotree vars --liveonly --space jam --controller citests robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline Must Have ROBOCORP_HOME= - Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) @@ -107,6 +106,7 @@ Goal: Liveonly works and uses virtual holotree Must Have RCC_ENVIRONMENT_HASH= Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHON_EXE= Wont Have PYTHONPATH= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= From ee4139f36cd864f515646f0efca5ef2084bb5993 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Mar 2022 13:26:52 +0200 Subject: [PATCH 220/516] BUGFIX: GH#27 still wrong PYTHON_EXE path (v11.6.5) - Still continuing GH#27 fixing issue where rcc finds executables outside of holotree environment. --- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot/robot.go | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index a1c86565..d58c643a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.4` + Version = `v11.6.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5688a6ea..af90a6a5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.6.5 (date: 2.3.2022) + +- Still continuing GH#27 fixing issue where rcc finds executables outside of + holotree environment. + ## v11.6.4 (date: 23.2.2022) - GH#27 fixing issue where rcc finds executables outside of holotree diff --git a/robot/robot.go b/robot/robot.go index d25cb6fd..8b61cfce 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -420,14 +420,15 @@ func (it *robot) ExecutionEnvironment(location string, inject []string, full boo environment = append(environment, os.Environ()...) } environment = append(environment, inject...) - searchPath := it.SearchPath(location) - python, ok := searchPath.Which("python3", conda.FileExtensions) + holotreePath := conda.HolotreePath(location) + python, ok := holotreePath.Which("python3", conda.FileExtensions) if !ok { - python, ok = searchPath.Which("python", conda.FileExtensions) + python, ok = holotreePath.Which("python", conda.FileExtensions) } if ok { environment = append(environment, "PYTHON_EXE="+python) } + searchPath := it.SearchPath(location) environment = append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, From fed1a424ca8fc5e9686690727502294d0780a936 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 7 Mar 2022 08:43:01 +0200 Subject: [PATCH 221/516] BUGFIX: ignoring .vscode in YAML/JSON diagnostics (v11.6.6) - JSON/YAML diagnostics is now ignoring anything that contains ".vscode" --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/diagnostics.go | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index d58c643a..433adca9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.5` + Version = `v11.6.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index af90a6a5..a4fd94c3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.6.6 (date: 7.3.2022) + +- JSON/YAML diagnostics is now ignoring anything that contains ".vscode" + ## v11.6.5 (date: 2.3.2022) - Still continuing GH#27 fixing issue where rcc finds executables outside of diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 180b9bae..0d9262ea 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -35,6 +35,20 @@ const ( statusFatal = `fatal` ) +var ( + ignorePathContains = []string{".vscode", ".ipynb_checkpoints", ".virtual_documents"} +) + +func shouldIgnorePath(fullpath string) bool { + lowpath := strings.ToLower(fullpath) + for _, ignore := range ignorePathContains { + if strings.Contains(lowpath, ignore) { + return true + } + } + return false +} + type stringerr func() (string, error) func justText(source stringerr) string { @@ -330,7 +344,7 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str for _, tail := range paths { investigated = true fullpath := filepath.Join(rootdir, tail) - if strings.Contains(fullpath, ".ipynb_checkpoints") || strings.Contains(fullpath, ".virtual_documents") { + if shouldIgnorePath(fullpath) { continue } content, err := ioutil.ReadFile(fullpath) From 3967b27defb7d2f5dab1a6ccba59905d79f68439 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Mar 2022 08:16:14 +0200 Subject: [PATCH 222/516] UPGRADE: started to use micromamba 0.22.0 (v11.7.0) - micromamba update to version 0.22.0 --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 4 ++++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 433adca9..8927f2aa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.6.6` + Version = `v11.7.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 59c99625..8bafada1 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.19.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.22.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index c8c8de14..83d39e2d 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.19.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.22.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 66a8607a..99931ddb 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.19.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.22.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 2b68487d..bce5042b 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -166,7 +166,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 19000 + goodEnough := version >= 22000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index a4fd94c3..2c4dce05 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.7.0 (date: 8.3.2022) + +- micromamba update to version 0.22.0 + ## v11.6.6 (date: 7.3.2022) - JSON/YAML diagnostics is now ignoring anything that contains ".vscode" From 7d17f965f0ee163f57abe0ddb0a48a7c2c8c7bba Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Mar 2022 09:31:03 +0200 Subject: [PATCH 223/516] BUGFIX: debugging related fixes (v11.7.1) - when timeline option is given, and operation fails, timeline was not shown and this change now makes timeline happen before exit is done - speed test now allows using debug flag to actually see what is going on --- cmd/rcc/main.go | 5 ++--- cmd/speed.go | 16 +++++++++++----- common/version.go | 2 +- docs/changelog.md | 6 ++++++ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 944a1c0a..ff00bf83 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -87,11 +87,10 @@ func markTempForRecycling() { } func main() { - common.TimelineBegin("Start.") - defer common.EndOfTimeline() - defer ExitProtection() + common.TimelineBegin("Start.") + defer common.EndOfTimeline() go startTempRecycling() defer markTempForRecycling() defer os.Stderr.Sync() diff --git a/cmd/speed.go b/cmd/speed.go index cd76948a..6862bce1 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -55,10 +55,12 @@ var speedtestCmd = &cobra.Command{ common.Log("This may take several minutes, please be patient.") signal := make(chan bool) timing := make(chan int) - go workingWorm(signal, timing) silent, trace, debug := common.Silent, common.TraceFlag, common.DebugFlag - common.Silent = true - common.UnifyVerbosityFlags() + if !debug { + go workingWorm(signal, timing) + common.Silent = true + common.UnifyVerbosityFlags() + } folder := common.RobocorpTemp() content, err := blobs.Asset("assets/speedtest.yaml") if err != nil { @@ -83,8 +85,12 @@ var speedtestCmd = &cobra.Command{ } score.Done() close(signal) - elapsed := <-timing - common.Log("%s", score.Score(elapsed)) + if !debug { + elapsed := <-timing + common.Log("%s", score.Score(elapsed)) + } else { + common.Log("%s", score.Score(0.0)) + } pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index 8927f2aa..abdafa9f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.7.0` + Version = `v11.7.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2c4dce05..49251686 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.7.1 (date: 8.3.2022) + +- when timeline option is given, and operation fails, timeline was not shown + and this change now makes timeline happen before exit is done +- speed test now allows using debug flag to actually see what is going on + ## v11.7.0 (date: 8.3.2022) - micromamba update to version 0.22.0 From fb74f1030422d83b391e1d75217889d92b92273e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Mar 2022 13:44:48 +0200 Subject: [PATCH 224/516] FEATURE: pre-run script support (v11.8.0) - added initial alpha support for pre-run scripts from robot.yaml and executed right before actual task is run --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 18 ++++++++++++++++++ robot/robot.go | 6 ++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index abdafa9f..aab0bb5a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.7.1` + Version = `v11.8.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 49251686..d6afb456 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.8.0 (date: 8.3.2022) + +- added initial alpha support for pre-run scripts from robot.yaml and executed + right before actual task is run + ## v11.7.1 (date: 8.3.2022) - when timeline option is given, and operation fails, timeline was not shown diff --git a/operations/running.go b/operations/running.go index 12148ba4..1331a966 100644 --- a/operations/running.go +++ b/operations/running.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/google/shlex" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" @@ -244,6 +245,23 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } FreezeEnvironmentListing(label, config) + + preRunScripts := config.PreRunScripts() + if preRunScripts != nil && len(preRunScripts) > 0 { + common.Debug("=== pre run script phase ===") + for _, script := range preRunScripts { + scriptCommand, err := shlex.Split(script) + if err != nil { + pretty.Exit(11, "%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) + } + common.Debug("Running pre run script '%s' ...", script) + _, err = shell.New(environment, directory, scriptCommand...).Execute(interactive) + if err != nil { + pretty.Exit(12, "%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) + } + } + } + common.Debug("about to run command - %v", task) if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) diff --git a/robot/robot.go b/robot/robot.go index 8b61cfce..f6acd177 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -32,6 +32,7 @@ type Robot interface { TaskByName(string) Task UsesConda() bool CondaConfigFile() string + PreRunScripts() []string RootDirectory() string HasHolozip() bool Holozip() string @@ -55,6 +56,7 @@ type Task interface { type robot struct { Tasks map[string]*task `yaml:"tasks"` Conda string `yaml:"condaConfigFile,omitempty"` + PreRun []string `yaml:"preRunScripts,omitempty"` Environments []string `yaml:"environmentConfigs,omitempty"` Ignored []string `yaml:"ignoreFiles"` Artifacts string `yaml:"artifactsDir"` @@ -330,6 +332,10 @@ func (it *robot) CondaConfigFile() string { return filepath.Join(it.Root, it.Conda) } +func (it *robot) PreRunScripts() []string { + return it.PreRun +} + func (it *robot) WorkingDirectory() string { return it.Root } From 4ca540c0cc658e988946b26c5b82b94658e337ea Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 9 Mar 2022 12:01:57 +0200 Subject: [PATCH 225/516] WIP: network configuration workflow (v11.9.0) - new work started around network configuration topics and this will be WIP (work in progress) for a while, and so it is marked as unstable - added new command placeholders (no-op): `interactive configuration`, `configuration export`, `configuration import`, and `configuration switch` --- cmd/configureexport.go | 31 ++++++++++++++++++++++++++++ cmd/configureimport.go | 25 ++++++++++++++++++++++ cmd/configureswitch.go | 25 ++++++++++++++++++++++ cmd/wizardconfig.go | 32 ++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 7 +++++++ settings/data.go | 14 +++++++++++++ wizard/common.go | 34 ++++++++++++++++++++++++++++++ wizard/config.go | 47 ++++++++++++++++++++++++++++++++++++++++++ wizard/create.go | 24 --------------------- 10 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 cmd/configureexport.go create mode 100644 cmd/configureimport.go create mode 100644 cmd/configureswitch.go create mode 100644 cmd/wizardconfig.go create mode 100644 wizard/config.go diff --git a/cmd/configureexport.go b/cmd/configureexport.go new file mode 100644 index 00000000..71e49c24 --- /dev/null +++ b/cmd/configureexport.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + configFile string + profileName string +) + +var configureExportCmd = &cobra.Command{ + Use: "export", + Short: "Export a configuration profile for Robocorp tooling.", + Long: "Export a configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Configuration export lasted").Report() + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(configureExportCmd) + configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "local_config.yaml", "The filename where configuration profile is exported.") + configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "unknown", "The name of configuration profile to export.") +} diff --git a/cmd/configureimport.go b/cmd/configureimport.go new file mode 100644 index 00000000..638e9dbb --- /dev/null +++ b/cmd/configureimport.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var configureImportCmd = &cobra.Command{ + Use: "import", + Short: "Import a configuration profile for Robocorp tooling.", + Long: "Import a configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Configuration import lasted").Report() + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(configureImportCmd) + configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "local_config.yaml", "The filename to import as configuration profile.") +} diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go new file mode 100644 index 00000000..56a3e332 --- /dev/null +++ b/cmd/configureswitch.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var configureSwitchCmd = &cobra.Command{ + Use: "switch", + Short: "Switch active configuration profile for Robocorp tooling.", + Long: "Switch active configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Configuration switch lasted").Report() + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(configureSwitchCmd) + configureSwitchCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to activate.") +} diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go new file mode 100644 index 00000000..b198ce02 --- /dev/null +++ b/cmd/wizardconfig.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/wizard" + + "github.com/spf13/cobra" +) + +var wizardConfigCommand = &cobra.Command{ + Use: "configuration", + Aliases: []string{"conf", "config", "configure"}, + Short: "Create a configuration profile for Robocorp tooling interactively.", + Long: "Create a configuration profile for Robocorp tooling interactively.", + Run: func(cmd *cobra.Command, args []string) { + if !pretty.Interactive { + pretty.Exit(1, "This is for interactive use only. Do not use in scripting/CI!") + } + if common.DebugFlag { + defer common.Stopwatch("Interactive configuration lasted").Report() + } + err := wizard.Configure(args) + if err != nil { + pretty.Exit(2, "%v", err) + } + }, +} + +func init() { + interactiveCmd.AddCommand(wizardConfigCommand) +} diff --git a/common/version.go b/common/version.go index aab0bb5a..9ae10009 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.8.0` + Version = `v11.9.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index d6afb456..59104050 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.9.0 (date: 9.3.2022) UNSTABLE + +- new work started around network configuration topics and this will be + WIP (work in progress) for a while, and so it is marked as unstable +- added new command placeholders (no-op): `interactive configuration`, + `configuration export`, `configuration import`, and `configuration switch` + ## v11.8.0 (date: 8.3.2022) - added initial alpha support for pre-run scripts from robot.yaml and executed diff --git a/settings/data.go b/settings/data.go index 892cc22d..53920c6e 100644 --- a/settings/data.go +++ b/settings/data.go @@ -209,3 +209,17 @@ type Meta struct { Source string `yaml:"source" json:"source"` Version string `yaml:"version" json:"version"` } + +type Network struct { + HttpsProxy string `yaml:"https-proxy" json:"https-proxy"` + HttpProxy string `yaml:"http-proxy" json:"http-proxy"` +} + +type Profile struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` + Network *Network `yaml:"network,omitempty" json:"network,omitempty"` + Piprc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` + Condarc string `yaml:"condarc,omitempty" json:"condarc,omitempty"` +} diff --git a/wizard/common.go b/wizard/common.go index ad1a851a..ba528f8e 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -1,10 +1,19 @@ package wizard import ( + "bufio" + "os" + "regexp" + "strings" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" ) +var ( + namePattern = regexp.MustCompile("^[\\w-]*$") +) + type WizardFn func([]string) error func warning(condition bool, message string) { @@ -19,3 +28,28 @@ func firstOf(arguments []string, missing string) string { } return missing } + +func note(message string) { + common.Stdout("%s! %s%s%s\n", pretty.Red, pretty.White, message, pretty.Reset) +} + +func ask(question, defaults string, validator *regexp.Regexp, erratic string) (string, error) { + for { + common.Stdout("%s? %s%s %s[%s]:%s ", pretty.Green, pretty.White, question, pretty.Grey, defaults, pretty.Reset) + source := bufio.NewReader(os.Stdin) + reply, err := source.ReadString(newline) + common.Stdout("\n") + if err != nil { + return "", err + } + reply = strings.TrimSpace(reply) + if !validator.MatchString(reply) { + common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + continue + } + if len(reply) == 0 { + return defaults, nil + } + return reply, nil + } +} diff --git a/wizard/config.go b/wizard/config.go new file mode 100644 index 00000000..72509fbb --- /dev/null +++ b/wizard/config.go @@ -0,0 +1,47 @@ +package wizard + +import ( + "fmt" + "regexp" + + "github.com/robocorp/rcc/common" +) + +var ( + proxyPattern = regexp.MustCompile("^(?:http[\\S]*)?$") + anyPattern = regexp.MustCompile("^\\s*\\S+") +) + +func Configure(arguments []string) error { + common.Stdout("\n") + + note("You are now configuring a profile to be used in Robocorp toolchain.\n") + + warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") + profileName, err := ask("Give profile a name", firstOf(arguments, "company"), namePattern, "Use just normal english word characters and no spaces!") + + if err != nil { + return err + } + + description, err := ask("Give a short description of this profile", "undocumented", anyPattern, "Description cannot be empty!") + + if err != nil { + return err + } + + httpsProxy, err := ask("URL for https proxy", "", proxyPattern, "Must be empty or start with 'http' and should not contain spaces!") + + if err != nil { + return err + } + + httpProxy, err := ask("URL for http proxy", httpsProxy, proxyPattern, "Must be empty or start with 'http' and should not contain spaces!") + + if err != nil { + return err + } + + fmt.Sprintf("%s%v%v%v", description, profileName, httpsProxy, httpProxy) + return nil +} diff --git a/wizard/create.go b/wizard/create.go index 0532e21f..e2d76670 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -1,9 +1,7 @@ package wizard import ( - "bufio" "fmt" - "os" "path/filepath" "regexp" "sort" @@ -21,31 +19,9 @@ const ( ) var ( - namePattern = regexp.MustCompile("^[\\w-]*$") digitPattern = regexp.MustCompile("^\\d+$") ) -func ask(question, defaults string, validator *regexp.Regexp, erratic string) (string, error) { - for { - common.Stdout("%s? %s%s %s[%s]:%s ", pretty.Green, pretty.White, question, pretty.Grey, defaults, pretty.Reset) - source := bufio.NewReader(os.Stdin) - reply, err := source.ReadString(newline) - common.Stdout("\n") - if err != nil { - return "", err - } - reply = strings.TrimSpace(reply) - if !validator.MatchString(reply) { - common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) - continue - } - if len(reply) == 0 { - return defaults, nil - } - return reply, nil - } -} - func choose(question, label string, candidates []string) (string, error) { keys := []string{} common.Stdout("%s%s:%s\n", pretty.Grey, label, pretty.Reset) From 28136f20750d035ec9336cebfd10067b1c7d3c38 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Mar 2022 11:22:20 +0200 Subject: [PATCH 226/516] WIP: asking more configuration values (v11.9.1) - added condarc and piprc to be asked from user as configuration options - refactoring some wizard code to better support new functionality --- cmd/wizardconfig.go | 6 +++++ common/version.go | 2 +- docs/changelog.md | 5 ++++ wizard/common.go | 31 ++++++++++++++++++++--- wizard/config.go | 61 ++++++++++++++++++++++++++++++++------------- wizard/create.go | 4 +-- 6 files changed, 86 insertions(+), 23 deletions(-) diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go index b198ce02..32908cd6 100644 --- a/cmd/wizardconfig.go +++ b/cmd/wizardconfig.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/wizard" @@ -24,6 +25,11 @@ var wizardConfigCommand = &cobra.Command{ if err != nil { pretty.Exit(2, "%v", err) } + _, err = operations.ProduceDiagnostics("", "", false, true) + if err != nil { + pretty.Exit(3, "Error: %v", err) + } + pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index 9ae10009..50fa1216 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.0` + Version = `v11.9.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 59104050..ca84a9e3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.9.1 (date: 10.3.2022) UNSTABLE + +- added condarc and piprc to be asked from user as configuration options +- refactoring some wizard code to better support new functionality + ## v11.9.0 (date: 9.3.2022) UNSTABLE - new work started around network configuration topics and this will be diff --git a/wizard/common.go b/wizard/common.go index ba528f8e..c0d7e86f 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" ) @@ -14,8 +15,33 @@ var ( namePattern = regexp.MustCompile("^[\\w-]*$") ) +type Validator func(string) bool + type WizardFn func([]string) error +func regexpValidation(validator *regexp.Regexp, erratic string) Validator { + return func(input string) bool { + if !validator.MatchString(input) { + common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + return false + } + return true + } +} + +func optionalFileValidation(erratic string) Validator { + return func(input string) bool { + if len(strings.TrimSpace(input)) == 0 { + return true + } + if !pathlib.IsFile(input) { + common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + return false + } + return true + } +} + func warning(condition bool, message string) { if condition { common.Stdout("%s%s%s\n\n", pretty.Yellow, message, pretty.Reset) @@ -33,7 +59,7 @@ func note(message string) { common.Stdout("%s! %s%s%s\n", pretty.Red, pretty.White, message, pretty.Reset) } -func ask(question, defaults string, validator *regexp.Regexp, erratic string) (string, error) { +func ask(question, defaults string, validator Validator) (string, error) { for { common.Stdout("%s? %s%s %s[%s]:%s ", pretty.Green, pretty.White, question, pretty.Grey, defaults, pretty.Reset) source := bufio.NewReader(os.Stdin) @@ -43,8 +69,7 @@ func ask(question, defaults string, validator *regexp.Regexp, erratic string) (s return "", err } reply = strings.TrimSpace(reply) - if !validator.MatchString(reply) { - common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + if !validator(reply) { continue } if len(reply) == 0 { diff --git a/wizard/config.go b/wizard/config.go index 72509fbb..9ca38992 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -12,36 +12,63 @@ var ( anyPattern = regexp.MustCompile("^\\s*\\S+") ) -func Configure(arguments []string) error { - common.Stdout("\n") +type question struct { + Identity string + Question string + Validator Validator +} - note("You are now configuring a profile to be used in Robocorp toolchain.\n") +type questions []question - warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - profileName, err := ask("Give profile a name", firstOf(arguments, "company"), namePattern, "Use just normal english word characters and no spaces!") +type answers map[string]string - if err != nil { - return err +func questionaire(questions questions, answers answers) error { + for _, question := range questions { + previous := answers[question.Identity] + indirect, ok := answers[previous] + if ok { + previous = indirect + } + answer, err := ask(question.Question, previous, question.Validator) + if err != nil { + return err + } + answers[question.Identity] = answer } + return nil +} - description, err := ask("Give a short description of this profile", "undocumented", anyPattern, "Description cannot be empty!") +func Configure(arguments []string) error { + common.Stdout("\n") - if err != nil { - return err - } + note("You are now configuring a profile to be used in Robocorp toolchain.\n") - httpsProxy, err := ask("URL for https proxy", "", proxyPattern, "Must be empty or start with 'http' and should not contain spaces!") + warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - if err != nil { - return err - } + answers := make(answers) + answers["profile-name"] = firstOf(arguments, "company") + answers["profile-description"] = "undocumented" + answers["https-proxy"] = "" + answers["http-proxy"] = "https-proxy" + answers["settings-yaml"] = "" + answers["conda-rc"] = "" + answers["pip-rc"] = "" - httpProxy, err := ask("URL for http proxy", httpsProxy, proxyPattern, "Must be empty or start with 'http' and should not contain spaces!") + err := questionaire(questions{ + {"profile-name", "Give profile a name", regexpValidation(namePattern, "Use just normal english word characters and no spaces!")}, + {"profile-description", "Give a short description of this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, + {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, + {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, + {"settings-yaml", "Path to settings.yaml file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"conda-rc", "Path to condarc file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"pip-rc", "Path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, + }, answers) if err != nil { return err } - fmt.Sprintf("%s%v%v%v", description, profileName, httpsProxy, httpProxy) + fmt.Println(answers) + return nil } diff --git a/wizard/create.go b/wizard/create.go index e2d76670..3857de27 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -36,7 +36,7 @@ func choose(question, label string, candidates []string) (string, error) { if err != nil { return "", err } - reply, err := ask(question, "1", pattern, "Give selections number from above list.") + reply, err := ask(question, "1", regexpValidation(pattern, "Give selections number from above list.")) if err != nil { return "", err } @@ -51,7 +51,7 @@ func Create(arguments []string) error { common.Stdout("\n") warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - robotName, err := ask("Give robot name", firstOf(arguments, "my-first-robot"), namePattern, "Use just normal english word characters and no spaces!") + robotName, err := ask("Give robot name", firstOf(arguments, "my-first-robot"), regexpValidation(namePattern, "Use just normal english word characters and no spaces!")) if err != nil { return err From e553b7e8e219bb56d9b566d624e21d84f45c69c6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 18 Mar 2022 11:12:39 +0200 Subject: [PATCH 227/516] WIP: refactoring layered settings (v11.9.2) - settings are now layered, so that partial custom settings.yaml also works - settings now have flat API interface, that is used instead of direct access - settings.yaml version upgrade with new fields (still incomplete) - endpoints in settings are now a map and not separate structure anymore - partial "demo" work on interactive configuration (work in progress) --- assets/settings.yaml | 10 +- common/version.go | 2 +- docs/changelog.md | 8 ++ settings/api.go | 31 ++++++ settings/data.go | 201 +++++++++++++++++++++++++++++++------- settings/settings.go | 102 +++++++++++++------ settings/settings_test.go | 17 ++++ wizard/config.go | 76 +++++++++++--- 8 files changed, 367 insertions(+), 80 deletions(-) create mode 100644 settings/api.go diff --git a/assets/settings.yaml b/assets/settings.yaml index f0b3bd83..7e5894c5 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -24,11 +24,19 @@ autoupdates: certificates: verify-ssl: true + no-revocation: false + custom-cert: # no custom certificate by default + +network: + https-proxy: # no proxy by default + http-proxy: # no proxy by default branding: logo: https://downloads.robocorp.com/company/press-kit/logos/robocorp-logo-black.svg theme-color: FF0000 meta: + name: default + description: default settings.yaml internal to rcc source: builtin - version: 2021.10 + version: 2022.03 diff --git a/common/version.go b/common/version.go index 50fa1216..c3736745 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.1` + Version = `v11.9.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index ca84a9e3..2ad5c292 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.9.2 (date: 18.3.2022) UNSTABLE + +- settings are now layered, so that partial custom settings.yaml also works +- settings now have flat API interface, that is used instead of direct access +- settings.yaml version upgrade with new fields (still incomplete) +- endpoints in settings are now a map and not separate structure anymore +- partial "demo" work on interactive configuration (work in progress) + ## v11.9.1 (date: 10.3.2022) UNSTABLE - added condarc and piprc to be asked from user as configuration options diff --git a/settings/api.go b/settings/api.go new file mode 100644 index 00000000..159c1292 --- /dev/null +++ b/settings/api.go @@ -0,0 +1,31 @@ +package settings + +import ( + "net/http" + + "github.com/robocorp/rcc/common" +) + +type EndpointsApi func(string) string + +type Api interface { + Name() string + Description() string + TemplatesYamlURL() string + Diagnostics(target *common.DiagnosticStatus) + Endpoint(string) string + DefaultEndpoint() string + IssuesURL() string + TelemetryURL() string + PypiURL() string + PypiTrustedHost() string + CondaURL() string + DownloadsLink(resource string) string + DocsLink(page string) string + PypiLink(page string) string + CondaLink(page string) string + Hostnames() []string + ConfiguredHttpTransport() *http.Transport + HttpsProxy() string + HttpProxy() string +} diff --git a/settings/data.go b/settings/data.go index 53920c6e..a8a93765 100644 --- a/settings/data.go +++ b/settings/data.go @@ -3,6 +3,7 @@ package settings import ( "encoding/json" "net/url" + "os" "sort" "strings" @@ -16,13 +17,57 @@ const ( type StringMap map[string]string +func (it StringMap) Lookup(key string) string { + return it[key] +} + +// layer 0 is defaults from assets +// layer 1 is settings.yaml from disk +// layer 2 is "temporary" update layer +type SettingsLayers [3]*Settings + +func (it SettingsLayers) Effective() *Settings { + result := &Settings{ + Autoupdates: make(StringMap), + Branding: make(StringMap), + Certificates: &Certificates{}, + Network: &Network{}, + Endpoints: make(StringMap), + Hosts: make([]string, 0, 100), + Meta: &Meta{ + Name: "generated", + Description: "generated", + Source: "generated", + Version: "unknown", + }, + } + for _, layer := range it { + if layer != nil { + layer.onTopOf(result) + } + } + return result +} + type Settings struct { - Autoupdates StringMap `yaml:"autoupdates" json:"autoupdates"` - Branding StringMap `yaml:"branding" json:"branding"` - Certificates *Certificates `yaml:"certificates" json:"certificates"` - Endpoints *Endpoints `yaml:"endpoints" json:"endpoints"` - Hosts []string `yaml:"diagnostics-hosts" json:"diagnostics-hosts"` - Meta *Meta `yaml:"meta" json:"meta"` + Autoupdates StringMap `yaml:"autoupdates,omitempty" json:"autoupdates,omitempty"` + Branding StringMap `yaml:"branding,omitempty" json:"branding,omitempty"` + Certificates *Certificates `yaml:"certificates,omitempty" json:"certificates,omitempty"` + Network *Network `yaml:"network,omitempty" json:"network,omitempty"` + Endpoints StringMap `yaml:"endpoints,omitempty" json:"endpoints,omitempty"` + Hosts []string `yaml:"diagnostics-hosts,omitempty" json:"diagnostics-hosts,omitempty"` + Meta *Meta `yaml:"meta,omitempty" json:"meta,omitempty"` +} + +func Empty() *Settings { + return &Settings{ + Meta: &Meta{ + Name: "generated", + Description: "generated", + Source: "generated", + Version: "unknown", + }, + } } func FromBytes(raw []byte) (*Settings, error) { @@ -34,6 +79,36 @@ func FromBytes(raw []byte) (*Settings, error) { return &settings, nil } +func (it *Settings) onTopOf(target *Settings) { + for key, value := range it.Autoupdates { + if len(value) > 0 { + target.Autoupdates[key] = value + } + } + for key, value := range it.Branding { + if len(value) > 0 { + target.Branding[key] = value + } + } + for key, value := range it.Endpoints { + if len(value) > 0 { + target.Endpoints[key] = value + } + } + for _, host := range it.Hosts { + target.Hosts = append(target.Hosts, host) + } + if it.Certificates != nil { + it.Certificates.onTopOf(target) + } + if it.Network != nil { + it.Network.onTopOf(target) + } + if it.Meta != nil { + it.Meta.onTopOf(target) + } +} + func (it *Settings) AsYaml() ([]byte, error) { content, err := yaml.Marshal(it) if err != nil { @@ -52,8 +127,8 @@ func (it *Settings) Source(filename string) *Settings { func (it *Settings) Hostnames() []string { collector := make(map[string]bool) if it.Endpoints != nil { - for _, name := range it.Endpoints.Hostnames() { - collector[name] = true + for _, name := range it.Endpoints { + hostFromUrl(name, collector) } } if it.Hosts != nil { @@ -109,8 +184,8 @@ func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStat diagnose.Fatal("", "endpoints section is totally missing") correct = false } else { - correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) + correct = diagnoseUrl(it.Endpoints["cloud-api"], "endpoints/cloud-api", diagnose, correct) + correct = diagnoseUrl(it.Endpoints["downloads"], "endpoints/downloads", diagnose, correct) } if correct { diagnose.Ok("Toplevel settings are ok.") @@ -128,17 +203,17 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { diagnose.Warning("", "settings.yaml: endpoints section is totally missing") correct = false } else { - correct = diagnoseUrl(it.Endpoints.CloudApi, "endpoints/cloud-api", diagnose, correct) - correct = diagnoseUrl(it.Endpoints.Downloads, "endpoints/downloads", diagnose, correct) - - correct = diagnoseOptionalUrl(it.Endpoints.CloudUi, "endpoints/cloud-ui", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.CloudLinking, "endpoints/cloud-linking", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.Issues, "endpoints/issues", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.Telemetry, "endpoints/telemetry", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.Docs, "endpoints/docs", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.Conda, "endpoints/conda", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.Pypi, "endpoints/pypi", diagnose, correct) - correct = diagnoseOptionalUrl(it.Endpoints.PypiTrusted, "endpoints/pypi-trusted", diagnose, correct) + correct = diagnoseUrl(it.Endpoints["cloud-api"], "endpoints/cloud-api", diagnose, correct) + correct = diagnoseUrl(it.Endpoints["downloads"], "endpoints/downloads", diagnose, correct) + + correct = diagnoseOptionalUrl(it.Endpoints["cloud-ui"], "endpoints/cloud-ui", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["cloud-linking"], "endpoints/cloud-linking", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["issues"], "endpoints/issues", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["telemetry"], "endpoints/telemetry", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["docs"], "endpoints/docs", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["conda"], "endpoints/conda", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["pypi"], "endpoints/pypi", diagnose, correct) + correct = diagnoseOptionalUrl(it.Endpoints["pypi-trusted"], "endpoints/pypi-trusted", diagnose, correct) } if it.Meta == nil { diagnose.Warning("", "settings.yaml: meta section is totally missing") @@ -153,17 +228,24 @@ type Certificates struct { VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` } +func (it *Certificates) onTopOf(target *Settings) { + if target.Certificates == nil { + target.Certificates = &Certificates{} + } + target.Certificates.VerifySsl = it.VerifySsl +} + type Endpoints struct { - CloudApi string `yaml:"cloud-api" json:"cloud-api"` - CloudLinking string `yaml:"cloud-linking" json:"cloud-linking"` - CloudUi string `yaml:"cloud-ui" json:"cloud-ui"` - Conda string `yaml:"conda" json:"conda"` - Docs string `yaml:"docs" json:"docs"` - Downloads string `yaml:"downloads" json:"downloads"` - Issues string `yaml:"issues" json:"issues"` - Pypi string `yaml:"pypi" json:"pypi"` - PypiTrusted string `yaml:"pypi-trusted" json:"pypi-trusted"` - Telemetry string `yaml:"telemetry" json:"telemetry"` + CloudApi string `yaml:"cloud-api,omitempty" json:"cloud-api,omitempty"` + CloudLinking string `yaml:"cloud-linking,omitempty" json:"cloud-linking,omitempty"` + CloudUi string `yaml:"cloud-ui,omitempty" json:"cloud-ui,omitempty"` + Conda string `yaml:"conda,omitempty" json:"conda,omitempty"` + Docs string `yaml:"docs,omitempty" json:"docs,omitempty"` + Downloads string `yaml:"downloads,omitempty" json:"downloads,omitempty"` + Issues string `yaml:"issues,omitempty" json:"issues,omitempty"` + Pypi string `yaml:"pypi,omitempty" json:"pypi,omitempty"` + PypiTrusted string `yaml:"pypi-trusted,omitempty" json:"pypi-trusted,omitempty"` + Telemetry string `yaml:"telemetry,omitempty" json:"telemetry,omitempty"` } func justHostAndPort(link string) string { @@ -206,8 +288,28 @@ func (it *Endpoints) Hostnames() []string { } type Meta struct { - Source string `yaml:"source" json:"source"` - Version string `yaml:"version" json:"version"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Source string `yaml:"source" json:"source"` + Version string `yaml:"version" json:"version"` +} + +func (it *Meta) onTopOf(target *Settings) { + if target.Meta == nil { + target.Meta = &Meta{} + } + if len(it.Name) > 0 { + target.Meta.Name = it.Name + } + if len(it.Description) > 0 { + target.Meta.Description = it.Description + } + if len(it.Source) > 0 { + target.Meta.Source = it.Source + } + if len(it.Version) > 0 { + target.Meta.Version = it.Version + } } type Network struct { @@ -215,11 +317,38 @@ type Network struct { HttpProxy string `yaml:"http-proxy" json:"http-proxy"` } +func (it *Network) onTopOf(target *Settings) { + if target.Network == nil { + target.Network = &Network{} + } + if len(it.HttpsProxy) > 0 { + target.Network.HttpsProxy = it.HttpsProxy + } + if len(it.HttpProxy) > 0 { + target.Network.HttpProxy = it.HttpProxy + } +} + type Profile struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` - Network *Network `yaml:"network,omitempty" json:"network,omitempty"` - Piprc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` - Condarc string `yaml:"condarc,omitempty" json:"condarc,omitempty"` + PipRc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` + CondaRc string `yaml:"condarc,omitempty" json:"condarc,omitempty"` +} + +func (it *Profile) AsYaml() ([]byte, error) { + content, err := yaml.Marshal(it) + if err != nil { + return nil, err + } + return content, nil +} + +func (it *Profile) SaveAs(filename string) error { + body, err := it.AsYaml() + if err != nil { + return err + } + return os.WriteFile(filename, body, 0o666) } diff --git a/settings/settings.go b/settings/settings.go index 25f04ade..d8899a5a 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -25,6 +25,7 @@ var ( httpTransport *http.Transport cachedSettings *Settings Global gateway + chain SettingsLayers ) func cacheSettings(result *Settings) (*Settings, error) { @@ -46,30 +47,44 @@ func DefaultSettings() ([]byte, error) { return blobs.Asset("assets/settings.yaml") } -func rawSettings() (content []byte, location string, err error) { - if HasCustomSettings() { - location = SettingsFileLocation() - content, err = ioutil.ReadFile(location) - return content, location, err - } else { - content, err = DefaultSettings() - return content, "builtin", err - } +func DefaultSettingsLayer() *Settings { + content, err := DefaultSettings() + pretty.Guard(err == nil, 111, "Could not read default settings, reason: %v", err) + config, err := FromBytes(content) + pretty.Guard(err == nil, 111, "Could not parse default settings, reason: %v", err) + return config } -func SummonSettings() (*Settings, error) { - if cachedSettings != nil { - return cachedSettings, nil +func CustomSettingsLayer() *Settings { + if !HasCustomSettings() { + return nil } - content, source, err := rawSettings() + content, err := ioutil.ReadFile(SettingsFileLocation()) + pretty.Guard(err == nil, 111, "Could not read custom settings, reason: %v", err) + config, err := FromBytes(content) + pretty.Guard(err == nil, 111, "Could not parse custom settings, reason: %v", err) + return config +} + +func TemporalSettingsLayer(filename string) error { + content, err := ioutil.ReadFile(filename) if err != nil { - return nil, err + return err } config, err := FromBytes(content) if err != nil { - return nil, err + return err + } + chain[2] = config + cachedSettings = nil + return nil +} + +func SummonSettings() (*Settings, error) { + if cachedSettings != nil { + return cachedSettings, nil } - return cacheSettings(config.Source(source)) + return cacheSettings(chain.Effective()) } func showDiagnosticsChecks(sink io.Writer, details *common.DiagnosticStatus) { @@ -114,6 +129,18 @@ func resolveLink(link, page string) string { type gateway bool +func (it gateway) Name() string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Meta.Name +} + +func (it gateway) Description() string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Meta.Description +} + func (it gateway) TemplatesYamlURL() string { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) @@ -126,47 +153,59 @@ func (it gateway) Diagnostics(target *common.DiagnosticStatus) { config.Diagnostics(target) } -func (it gateway) Endpoints() *Endpoints { +func (it gateway) Endpoint(key string) string { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - pretty.Guard(config.Endpoints != nil, 111, "settings.yaml: endpoints are missing") - return config.Endpoints + //pretty.Guard(config.Endpoints != nil, 111, "settings.yaml: endpoints are missing") + return config.Endpoints[key] } func (it gateway) DefaultEndpoint() string { - return it.Endpoints().CloudApi + return it.Endpoint("cloud-api") } func (it gateway) IssuesURL() string { - return it.Endpoints().Issues + return it.Endpoint("issues") } func (it gateway) TelemetryURL() string { - return it.Endpoints().Telemetry + return it.Endpoint("telemetry") } func (it gateway) PypiURL() string { - return it.Endpoints().Pypi + return it.Endpoint("pypi") } func (it gateway) PypiTrustedHost() string { - return justHostAndPort(it.Endpoints().PypiTrusted) + return justHostAndPort(it.Endpoint("pypi-trusted")) } func (it gateway) CondaURL() string { - return it.Endpoints().Conda + return it.Endpoint("conda") +} + +func (it gateway) HttpsProxy() string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Network.HttpsProxy +} + +func (it gateway) HttpProxy() string { + config, err := SummonSettings() + pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) + return config.Network.HttpProxy } func (it gateway) DownloadsLink(resource string) string { - return resolveLink(it.Endpoints().Downloads, resource) + return resolveLink(it.Endpoint("downloads"), resource) } func (it gateway) DocsLink(page string) string { - return resolveLink(it.Endpoints().Docs, page) + return resolveLink(it.Endpoint("docs"), page) } func (it gateway) PypiLink(page string) string { - endpoint := it.Endpoints().Pypi + endpoint := it.Endpoint("pypi") if len(endpoint) == 0 { endpoint = pypiDefault } @@ -174,7 +213,7 @@ func (it gateway) PypiLink(page string) string { } func (it gateway) CondaLink(page string) string { - endpoint := it.Endpoints().Conda + endpoint := it.Endpoint("conda") if len(endpoint) == 0 { endpoint = condaDefault } @@ -192,6 +231,11 @@ func (it gateway) ConfiguredHttpTransport() *http.Transport { } func init() { + chain = SettingsLayers{ + DefaultSettingsLayer(), + CustomSettingsLayer(), + nil, + } verifySsl := true Global = gateway(true) httpTransport = http.DefaultTransport.(*http.Transport).Clone() diff --git a/settings/settings_test.go b/settings/settings_test.go index 96347f98..d3e77c1b 100644 --- a/settings/settings_test.go +++ b/settings/settings_test.go @@ -20,3 +20,20 @@ func TestCanCallEntropyFunction(t *testing.T) { must_be.Equal("https://robocorp.com/docs/hello.html", settings.Global.DocsLink("hello.html")) must_be.Equal("https://robocorp.com/docs/products/manual.html", settings.Global.DocsLink("products/manual.html")) } + +func TestThatSomeDefaultValuesAreVisible(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + sut, err := settings.SummonSettings() + must_be.Nil(err) + wont_be.Nil(sut) + + must_be.Equal("https://api.eu1.robocorp.com/", settings.Global.DefaultEndpoint()) + must_be.Equal("https://telemetry.robocorp.com/", settings.Global.TelemetryURL()) + must_be.Equal("", settings.Global.PypiURL()) + must_be.Equal("", settings.Global.PypiTrustedHost()) + must_be.Equal("", settings.Global.CondaURL()) + must_be.Equal("", settings.Global.HttpProxy()) + must_be.Equal("", settings.Global.HttpsProxy()) + must_be.Equal(10, len(settings.Global.Hostnames())) +} diff --git a/wizard/config.go b/wizard/config.go index 9ca38992..0868b47f 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -2,9 +2,13 @@ package wizard import ( "fmt" + "os" "regexp" + "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/settings" ) var ( @@ -42,33 +46,79 @@ func Configure(arguments []string) error { common.Stdout("\n") note("You are now configuring a profile to be used in Robocorp toolchain.\n") + answers := make(answers) warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - answers := make(answers) - answers["profile-name"] = firstOf(arguments, "company") - answers["profile-description"] = "undocumented" - answers["https-proxy"] = "" - answers["http-proxy"] = "https-proxy" - answers["settings-yaml"] = "" - answers["conda-rc"] = "" - answers["pip-rc"] = "" - - err := questionaire(questions{ + filename, err := ask("Path to (otional) settings.yaml", "", optionalFileValidation("Value should be valid file in filesystem.")) + if err != nil { + return err + } + if len(filename) > 0 { + settings.TemporalSettingsLayer(filename) + } + + answers["profile-name"] = firstOf(arguments, settings.Global.Name()) + answers["profile-description"] = settings.Global.Description() + answers["https-proxy"] = settings.Global.HttpsProxy() + answers["http-proxy"] = settings.Global.HttpProxy() + + err = questionaire(questions{ {"profile-name", "Give profile a name", regexpValidation(namePattern, "Use just normal english word characters and no spaces!")}, {"profile-description", "Give a short description of this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, - {"settings-yaml", "Path to settings.yaml file", optionalFileValidation("Value should be valid file in filesystem.")}, {"conda-rc", "Path to condarc file", optionalFileValidation("Value should be valid file in filesystem.")}, {"pip-rc", "Path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, }, answers) - if err != nil { return err } - fmt.Println(answers) + name := answers["profile-name"] + profile := &settings.Profile{ + Name: name, + Description: answers["profile-description"], + } + + blob, ok := pullFile(answers["settings-yaml"]) + if ok { + profile.Settings, _ = settings.FromBytes(blob) + } else { + profile.Settings = settings.Empty() + } + + profile.Settings.Network = &settings.Network{ + HttpsProxy: answers["https-proxy"], + HttpProxy: answers["http-proxy"], + } + + profile.Settings.Meta.Name = name + profile.Settings.Meta.Description = answers["profile-description"] + + blob, ok = pullFile(answers["conda-rc"]) + if ok { + profile.CondaRc = string(blob) + } + + blob, ok = pullFile(answers["pip-rc"]) + if ok { + profile.PipRc = string(blob) + } + + // FIXME: following is just temporary "work in progress" save + profile.SaveAs(fmt.Sprintf("profile_%s.yaml", strings.ToLower(name))) return nil } + +func pullFile(filename string) ([]byte, bool) { + if !pathlib.IsFile(filename) { + return nil, false + } + body, err := os.ReadFile(filename) + if err != nil { + return nil, false + } + return body, true +} From 728675f1c56444aaaff8ec7c0edd9d1dcbd21b78 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 22 Mar 2022 10:54:51 +0200 Subject: [PATCH 228/516] WIP: configuration profile continued (v11.9.3) - started to add real support for profile switching/importing - some documentation updates --- cmd/configureexport.go | 7 ++-- cmd/configureimport.go | 6 +++ cmd/configureswitch.go | 71 ++++++++++++++++++++++++++++++- common/variables.go | 12 ++++++ common/version.go | 2 +- docs/changelog.md | 5 +++ docs/features.md | 2 +- docs/recipes.md | 57 +++++++++++++++++++++++++ docs/usecases.md | 5 ++- settings/data.go | 25 ----------- settings/profile.go | 95 ++++++++++++++++++++++++++++++++++++++++++ settings/settings.go | 28 ++++++------- wizard/config.go | 6 +-- 13 files changed, 272 insertions(+), 49 deletions(-) create mode 100644 settings/profile.go diff --git a/cmd/configureexport.go b/cmd/configureexport.go index 71e49c24..569c0c61 100644 --- a/cmd/configureexport.go +++ b/cmd/configureexport.go @@ -8,8 +8,9 @@ import ( ) var ( - configFile string - profileName string + configFile string + profileName string + clearProfile bool ) var configureExportCmd = &cobra.Command{ @@ -26,6 +27,6 @@ var configureExportCmd = &cobra.Command{ func init() { configureCmd.AddCommand(configureExportCmd) - configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "local_config.yaml", "The filename where configuration profile is exported.") + configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename where configuration profile is exported.") configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "unknown", "The name of configuration profile to export.") } diff --git a/cmd/configureimport.go b/cmd/configureimport.go index 638e9dbb..f1d78eef 100644 --- a/cmd/configureimport.go +++ b/cmd/configureimport.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -15,6 +16,11 @@ var configureImportCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Configuration import lasted").Report() } + profile := &settings.Profile{} + err := profile.LoadFrom(configFile) + pretty.Guard(err == nil, 1, "Error while loading profile: %v", err) + err = profile.Import() + pretty.Guard(err == nil, 2, "Error while importing profile: %v", err) pretty.Ok() }, } diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go index 56a3e332..4e1e5bb9 100644 --- a/cmd/configureswitch.go +++ b/cmd/configureswitch.go @@ -1,12 +1,65 @@ package cmd import ( + "fmt" + "path/filepath" + "strings" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) +func profileMap() map[string]string { + pattern := common.ExpandPath(filepath.Join(common.RobocorpHome(), "profile_*.yaml")) + found, err := filepath.Glob(pattern) + pretty.Guard(err == nil, 1, "Error while searching profiles: %v", err) + result := make(map[string]string) + for _, name := range found { + profile := settings.Profile{} + err = profile.LoadFrom(name) + if err == nil { + result[profile.Name] = profile.Description + } + } + return result +} + +func jsonListProfiles() { + content, err := operations.NiceJsonOutput(profileMap()) + pretty.Guard(err == nil, 1, "Error serializing profiles: %v", err) + common.Stdout("%s\n", content) +} + +func listProfiles() { + profiles := profileMap() + pretty.Guard(len(profiles) > 0, 2, "No profiles found, you must first import some.") + common.Stdout("Available profiles:\n") + for name, description := range profiles { + common.Stdout("- %s: %s\n", name, description) + } + common.Stdout("\n") +} + +func switchProfileTo(name string) { + filename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) + fullpath := common.ExpandPath(filepath.Join(common.RobocorpHome(), filename)) + profile := settings.Profile{} + err := profile.LoadFrom(fullpath) + pretty.Guard(err == nil, 3, "Error while loading/parsing profile, reason: %v", err) + err = profile.Activate() + pretty.Guard(err == nil, 4, "Error while activating profile, reason: %v", err) +} + +func cleanupProfile() { + profile := settings.Profile{} + err := profile.Remove() + pretty.Guard(err == nil, 5, "Error while clearing profile, reason: %v", err) +} + var configureSwitchCmd = &cobra.Command{ Use: "switch", Short: "Switch active configuration profile for Robocorp tooling.", @@ -15,11 +68,27 @@ var configureSwitchCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Configuration switch lasted").Report() } - pretty.Ok() + if clearProfile { + cleanupProfile() + pretty.Ok() + } else if len(profileName) == 0 { + if jsonFlag { + jsonListProfiles() + } else { + listProfiles() + common.Stdout("Currently active profile is: %s\n", settings.Global.Name()) + pretty.Ok() + } + } else { + switchProfileTo(profileName) + pretty.Ok() + } }, } func init() { configureCmd.AddCommand(configureSwitchCmd) configureSwitchCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to activate.") + configureSwitchCmd.Flags().BoolVarP(&clearProfile, "noprofile", "n", false, "Remove active profile, and reset to defaults.") + configureSwitchCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show profile list as JSON stream.") } diff --git a/common/variables.go b/common/variables.go index c72e2a8e..fbb1ab31 100644 --- a/common/variables.go +++ b/common/variables.go @@ -154,6 +154,18 @@ func MambaPackages() string { return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) } +func PipRcFile() string { + return ExpandPath(filepath.Join(RobocorpHome(), "piprc")) +} + +func MicroMambaRcFile() string { + return ExpandPath(filepath.Join(RobocorpHome(), "micromambarc")) +} + +func SettingsFile() string { + return ExpandPath(filepath.Join(RobocorpHome(), "settings.yaml")) +} + func UnifyVerbosityFlags() { if Silent { DebugFlag = false diff --git a/common/version.go b/common/version.go index c3736745..b80f8d16 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.2` + Version = `v11.9.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2ad5c292..d7710593 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.9.3 (date: 22.3.2022) UNSTABLE + +- started to add real support for profile switching/importing +- some documentation updates + ## v11.9.2 (date: 18.3.2022) UNSTABLE - settings are now layered, so that partial custom settings.yaml also works diff --git a/docs/features.md b/docs/features.md index 1997e29f..03bf5c45 100644 --- a/docs/features.md +++ b/docs/features.md @@ -7,7 +7,7 @@ * easily run software robots (automations) based on declarative robot.yaml files * test robots in isolated environments before uploading them to Control Room * provide commands for Robocorp runtime and developer tools (Workforce Agent, - Assistant, Lab, Code, ...) + Assistant, Code, ...) * provides commands to communicate with Robocorp Control Room from command line * enable caching dormant environments in efficiently and activating them locally when required without need to reinstall anything diff --git a/docs/recipes.md b/docs/recipes.md index f047cbc0..aa972651 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -238,6 +238,7 @@ value there is `user`. Here it is up to user or application to decide their strategy of use of different names to separate environments to their logical used partitions. If you choose to use just defaults (user/user) then there is going to be only one real environment available. +``` But above three controls gives you good ways to control how you and your applications manage their usage of different python environments for @@ -264,6 +265,62 @@ Identity Controller Space Blueprint Full path 9e7018022_2daaa295 rcc.tricks tips c34ed96c2d8a459a /tmp/rchome/holotree/9e7018022_2daaa295 ``` +### How to get understanding on holotree? + +See: https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md + +### How to activate holotree environment? + +On Linux/MacOSX: + +``` +source <(rcc holotree variables --space mine --robot path/to/robot.yaml) +``` + +On Windows + +``` +rcc holotree variables --space mine --robot path/to/robot.yaml > mine_activate.bat +call mine_activate.bat +``` + +You can also try + +``` +rcc task shell --robot path/to/robot.yaml +``` + +## What can be controlled using environment variables? + +- `ROBOCORP_HOME` points to directory where rcc keeps most of Robocorp related + files and directories are kept +- `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` makes rcc more relaxed on system + requirements (like long path support requirement on Windows) but it also + means that if set, responsibility of resolving failures are on user side +- `RCC_VERBOSE_ENVIRONMENT_BUILDING` makes environment creation more verbose, + so that failing environment creation can be seen with more details +- `RCC_CREDENTIALS_ID` is way to provide Control Room credentials using + environment variables + +## How to troubleshoot rcc setup and robots? + +```sh +# to get generic setup diagnostics +rcc configure diagnostics + +# to get robot and environment setup diagnostics +rcc configure diagnostics --robot path/to/robot.yaml + +# to see how well rcc performs in your machine +rcc configure speedtest +``` + +### Additional debugging options + +- generic flag `--debug` shows debug messages during execution +- generic flag `--trace` shows more verbose debugging messages during execution +- flag `--timeline` can be used to see execution timeline and where time was spent + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html diff --git a/docs/usecases.md b/docs/usecases.md index fb9636b0..89bec9f2 100644 --- a/docs/usecases.md +++ b/docs/usecases.md @@ -2,7 +2,7 @@ * run robots in Robocorp Workforce Agent locally or in cloud containers * run robots in Robocorp Assistant -* provide commands for Robocorp Lab and Code to develop robots locally and +* provide commands for Robocorp Code to develop robots locally and communicate to Robocorp Control Room * provide commands that can be used in CI pipelines (Jenkins, Gitlab CI, ...) to push robots into Robocorp Control Room @@ -10,6 +10,8 @@ * to use other scripting languages and tools available from conda-forge (or conda in general) with isolated and easily installed manner (see list below for ideas what is available) +* provide above things in computers, where internet access is restricted or + prohibited (using pre-made hololib.zip environments) ## What is available from conda-forge? @@ -20,6 +22,7 @@ * r and libraries * julia and libraries * make, cmake and compilers (C++, Fortran, ...) +* nginx * nodejs * rust * php diff --git a/settings/data.go b/settings/data.go index a8a93765..c04307f0 100644 --- a/settings/data.go +++ b/settings/data.go @@ -3,7 +3,6 @@ package settings import ( "encoding/json" "net/url" - "os" "sort" "strings" @@ -328,27 +327,3 @@ func (it *Network) onTopOf(target *Settings) { target.Network.HttpProxy = it.HttpProxy } } - -type Profile struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` - Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` - PipRc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` - CondaRc string `yaml:"condarc,omitempty" json:"condarc,omitempty"` -} - -func (it *Profile) AsYaml() ([]byte, error) { - content, err := yaml.Marshal(it) - if err != nil { - return nil, err - } - return content, nil -} - -func (it *Profile) SaveAs(filename string) error { - body, err := it.AsYaml() - if err != nil { - return err - } - return os.WriteFile(filename, body, 0o666) -} diff --git a/settings/profile.go b/settings/profile.go new file mode 100644 index 00000000..4193ae7b --- /dev/null +++ b/settings/profile.go @@ -0,0 +1,95 @@ +package settings + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" + "gopkg.in/yaml.v1" +) + +type Profile struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` + PipRc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` + MicroMambaRc string `yaml:"micromambarc,omitempty" json:"micromambarc,omitempty"` +} + +func (it *Profile) AsYaml() ([]byte, error) { + content, err := yaml.Marshal(it) + if err != nil { + return nil, err + } + return content, nil +} + +func (it *Profile) SaveAs(filename string) error { + body, err := it.AsYaml() + if err != nil { + return err + } + return os.WriteFile(filename, body, 0o666) +} + +func (it *Profile) LoadFrom(filename string) error { + raw, err := os.ReadFile(filename) + if err != nil { + return err + } + return yaml.Unmarshal(raw, it) +} + +func (it *Profile) Import() (err error) { + basename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(it.Name)) + filename := common.ExpandPath(filepath.Join(common.RobocorpHome(), basename)) + return it.SaveAs(filename) +} + +func (it *Profile) Activate() (err error) { + defer fail.Around(&err) + + err = it.Remove() + fail.On(err != nil, "%s", err) + if it.Settings != nil { + body, err := it.Settings.AsYaml() + fail.On(err != nil, "Failed to parse settings.yaml, reason: %v", err) + err = saveIfBody(common.SettingsFile(), body) + fail.On(err != nil, "Failed to save settings.yaml, reason: %v", err) + } + err = saveIfBody(common.PipRcFile(), []byte(it.PipRc)) + fail.On(err != nil, "Failed to save piprc, reason: %v", err) + err = saveIfBody(common.MicroMambaRcFile(), []byte(it.MicroMambaRc)) + fail.On(err != nil, "Failed to save micromambarc, reason: %v", err) + return nil +} + +func (it *Profile) Remove() (err error) { + defer fail.Around(&err) + + err = removeIfExists(common.PipRcFile()) + fail.On(err != nil, "Failed to remove piprc, reason: %v", err) + err = removeIfExists(common.MicroMambaRcFile()) + fail.On(err != nil, "Failed to remove micromambarc, reason: %v", err) + err = removeIfExists(common.SettingsFile()) + fail.On(err != nil, "Failed to remove settings.yaml, reason: %v", err) + return nil +} + +func removeIfExists(filename string) error { + if !pathlib.Exists(filename) { + return nil + } + return os.Remove(filename) +} + +func saveIfBody(filename string, body []byte) error { + if body != nil && len(body) > 0 { + return os.WriteFile(filename, body, 0o666) + } + return nil +} diff --git a/settings/settings.go b/settings/settings.go index d8899a5a..361fbce6 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "os" - "path/filepath" "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" @@ -35,12 +34,8 @@ func cacheSettings(result *Settings) (*Settings, error) { return result, nil } -func SettingsFileLocation() string { - return filepath.Join(common.RobocorpHome(), "settings.yaml") -} - func HasCustomSettings() bool { - return pathlib.IsFile(SettingsFileLocation()) + return pathlib.IsFile(common.SettingsFile()) } func DefaultSettings() ([]byte, error) { @@ -59,19 +54,25 @@ func CustomSettingsLayer() *Settings { if !HasCustomSettings() { return nil } - content, err := ioutil.ReadFile(SettingsFileLocation()) - pretty.Guard(err == nil, 111, "Could not read custom settings, reason: %v", err) - config, err := FromBytes(content) - pretty.Guard(err == nil, 111, "Could not parse custom settings, reason: %v", err) + config, err := LoadSetting(common.SettingsFile()) + pretty.Guard(err == nil, 111, "Could not load/parse custom settings, reason: %v", err) return config } -func TemporalSettingsLayer(filename string) error { +func LoadSetting(filename string) (*Settings, error) { content, err := ioutil.ReadFile(filename) if err != nil { - return err + return nil, err } config, err := FromBytes(content) + if err != nil { + return nil, err + } + return config, nil +} + +func TemporalSettingsLayer(filename string) error { + config, err := LoadSetting(filename) if err != nil { return err } @@ -104,7 +105,7 @@ func CriticalEnvironmentSettingsCheck() { config.CriticalEnvironmentDiagnostics(result) diagnose := result.Diagnose("Settings") if HasCustomSettings() { - diagnose.Ok("Uses custom settings at %q.", SettingsFileLocation()) + diagnose.Ok("Uses custom settings at %q.", common.SettingsFile()) } else { diagnose.Ok("Uses builtin settings.") } @@ -156,7 +157,6 @@ func (it gateway) Diagnostics(target *common.DiagnosticStatus) { func (it gateway) Endpoint(key string) string { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - //pretty.Guard(config.Endpoints != nil, 111, "settings.yaml: endpoints are missing") return config.Endpoints[key] } diff --git a/wizard/config.go b/wizard/config.go index 0868b47f..65076810 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -68,7 +68,7 @@ func Configure(arguments []string) error { {"profile-description", "Give a short description of this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, - {"conda-rc", "Path to condarc file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"micromamba-rc", "Path to micromambarc file", optionalFileValidation("Value should be valid file in filesystem.")}, {"pip-rc", "Path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, }, answers) if err != nil { @@ -96,9 +96,9 @@ func Configure(arguments []string) error { profile.Settings.Meta.Name = name profile.Settings.Meta.Description = answers["profile-description"] - blob, ok = pullFile(answers["conda-rc"]) + blob, ok = pullFile(answers["micromamba-rc"]) if ok { - profile.CondaRc = string(blob) + profile.MicroMambaRc = string(blob) } blob, ok = pullFile(answers["pip-rc"]) From 6ae8db57eadd9378c7bed2946a07cc4675ccd33c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 22 Mar 2022 15:45:29 +0200 Subject: [PATCH 229/516] WIP: configuration profile exporting (v11.9.4) - profile exporting now works --- cmd/configureexport.go | 6 +++++- cmd/configureimport.go | 3 ++- cmd/configureswitch.go | 11 ++++++++--- common/commander.go | 4 ++-- common/version.go | 2 +- conda/workflows.go | 4 +++- docs/changelog.md | 4 ++++ docs/recipes.md | 1 - operations/diagnostics.go | 6 ++++++ settings/api.go | 2 ++ settings/settings.go | 7 +++++++ 11 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cmd/configureexport.go b/cmd/configureexport.go index 569c0c61..f20374e1 100644 --- a/cmd/configureexport.go +++ b/cmd/configureexport.go @@ -21,6 +21,9 @@ var configureExportCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Configuration export lasted").Report() } + profile := loadNamedProfile(profileName) + err := profile.SaveAs(configFile) + pretty.Guard(err == nil, 1, "Error while exporting profile, reason: %v", err) pretty.Ok() }, } @@ -28,5 +31,6 @@ var configureExportCmd = &cobra.Command{ func init() { configureCmd.AddCommand(configureExportCmd) configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename where configuration profile is exported.") - configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "unknown", "The name of configuration profile to export.") + configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to export.") + configureExportCmd.MarkFlagRequired("profile") } diff --git a/cmd/configureimport.go b/cmd/configureimport.go index f1d78eef..76ed3723 100644 --- a/cmd/configureimport.go +++ b/cmd/configureimport.go @@ -27,5 +27,6 @@ var configureImportCmd = &cobra.Command{ func init() { configureCmd.AddCommand(configureImportCmd) - configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "local_config.yaml", "The filename to import as configuration profile.") + configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename to import as configuration profile.") + configureImportCmd.MarkFlagRequired("filename") } diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go index 4e1e5bb9..cd5fbfe1 100644 --- a/cmd/configureswitch.go +++ b/cmd/configureswitch.go @@ -44,13 +44,18 @@ func listProfiles() { common.Stdout("\n") } -func switchProfileTo(name string) { +func loadNamedProfile(name string) *settings.Profile { filename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) fullpath := common.ExpandPath(filepath.Join(common.RobocorpHome(), filename)) - profile := settings.Profile{} + profile := &settings.Profile{} err := profile.LoadFrom(fullpath) pretty.Guard(err == nil, 3, "Error while loading/parsing profile, reason: %v", err) - err = profile.Activate() + return profile +} + +func switchProfileTo(name string) { + profile := loadNamedProfile(name) + err := profile.Activate() pretty.Guard(err == nil, 4, "Error while activating profile, reason: %v", err) } diff --git a/common/commander.go b/common/commander.go index 1a37545c..a4da75ee 100644 --- a/common/commander.go +++ b/common/commander.go @@ -14,9 +14,9 @@ func (it *Commander) Option(name, value string) *Commander { return it } -func (it *Commander) ConditionalFlag(condition bool, name string) *Commander { +func (it *Commander) ConditionalFlag(condition bool, details ...string) *Commander { if condition { - it.command = append(it.command, name) + it.command = append(it.command, details...) } return it } diff --git a/common/version.go b/common/version.go index b80f8d16..05231e19 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.3` + Version = `v11.9.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index d900c086..c730278b 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -138,9 +138,11 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh ttl = "0" } common.Progress(5, "Running micromamba phase.") - mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) + mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") + mambaCommand.ConditionalFlag(!settings.Global.HasMicroMambaRc(), "--no-rc") + mambaCommand.ConditionalFlag(settings.Global.HasMicroMambaRc(), "--rc-file", common.MicroMambaRcFile()) observer := make(InstallObserver) common.Debug("=== micromamba create phase ===") fmt.Fprintf(planWriter, "\n--- micromamba plan @%ss ---\n\n", stopwatch) diff --git a/docs/changelog.md b/docs/changelog.md index d7710593..972637f3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.9.4 (date: 22.3.2022) UNSTABLE + +- profile exporting now works + ## v11.9.3 (date: 22.3.2022) UNSTABLE - started to add real support for profile switching/importing diff --git a/docs/recipes.md b/docs/recipes.md index aa972651..9a1caea2 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -238,7 +238,6 @@ value there is `user`. Here it is up to user or application to decide their strategy of use of different names to separate environments to their logical used partitions. If you choose to use just defaults (user/user) then there is going to be only one real environment available. -``` But above three controls gives you good ways to control how you and your applications manage their usage of different python environments for diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 0d9262ea..8ad6761d 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -78,6 +78,12 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["user-agent"] = common.UserAgent() result.Details["installationId"] = xviper.TrackingIdentity() result.Details["telemetry-enabled"] = fmt.Sprintf("%v", xviper.CanTrack()) + result.Details["config-piprc-used"] = fmt.Sprintf("%v", settings.Global.HasPipRc()) + result.Details["config-micromambarc-used"] = fmt.Sprintf("%v", settings.Global.HasMicroMambaRc()) + result.Details["config-settings-yaml-used"] = fmt.Sprintf("%v", pathlib.IsFile(common.SettingsFile())) + result.Details["config-active-profile"] = settings.Global.Name() + result.Details["config-https-proxy"] = settings.Global.HttpsProxy() + result.Details["config-http-proxy"] = settings.Global.HttpProxy() result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") diff --git a/settings/api.go b/settings/api.go index 159c1292..49db0bf4 100644 --- a/settings/api.go +++ b/settings/api.go @@ -28,4 +28,6 @@ type Api interface { ConfiguredHttpTransport() *http.Transport HttpsProxy() string HttpProxy() string + HasPipRc() bool + HasMicroMambaRc() bool } diff --git a/settings/settings.go b/settings/settings.go index 361fbce6..2ae33b2f 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -195,6 +195,13 @@ func (it gateway) HttpProxy() string { pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) return config.Network.HttpProxy } +func (it gateway) HasPipRc() bool { + return pathlib.IsFile(common.PipRcFile()) +} + +func (it gateway) HasMicroMambaRc() bool { + return pathlib.IsFile(common.MicroMambaRcFile()) +} func (it gateway) DownloadsLink(resource string) string { return resolveLink(it.Endpoint("downloads"), resource) From aa61da4b6072e82f99639846e7b10d21ef42ab9a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 23 Mar 2022 13:41:50 +0200 Subject: [PATCH 230/516] WIP: variable export refactoring (v11.9.5) - refactoring variables exporting into one place - adding `PIP_CONFIG_FILE`, `HTTP_PROXY`, and `HTTPS_PROXY` variables into conda environment if they are configured --- cmd/holotreeVariables.go | 6 +++--- common/version.go | 2 +- conda/robocorp.go | 32 +++++++++++++++++++++++------- conda/workflows.go | 2 +- docs/changelog.md | 6 ++++++ operations/running.go | 2 +- robot/robot.go | 42 +++++----------------------------------- 7 files changed, 42 insertions(+), 50 deletions(-) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index f9903e09..19a21cfc 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -90,11 +90,11 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp } common.Timeline("load robot environment") - env := conda.EnvironmentExtensionFor(path) + var env []string if config != nil { - env = config.ExecutionEnvironment(path, extra, false) + env = config.RobotExecutionEnvironment(path, extra, false) } else { - env = append(extra, env...) + env = conda.CondaExecutionEnvironment(path, extra, false) } if Has(workspace) { diff --git a/common/version.go b/common/version.go index 05231e19..085a8d02 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.4` + Version = `v11.9.5` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index bce5042b..ae20a61c 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" "github.com/robocorp/rcc/xviper" ) @@ -87,12 +88,18 @@ func FindPath(environment string) pathlib.PathParts { return target } -func EnvironmentExtensionFor(location string) []string { - environment := make([]string, 0, 20) - searchPath := HolotreePath(location) - python, ok := searchPath.Which("python3", FileExtensions) +func CondaExecutionEnvironment(location string, inject []string, full bool) []string { + environment := make([]string, 0, 100) + if full { + environment = append(environment, os.Environ()...) + } + if inject != nil && len(inject) > 0 { + environment = append(environment, inject...) + } + holotreePath := HolotreePath(location) + python, ok := holotreePath.Which("python3", FileExtensions) if !ok { - python, ok = searchPath.Which("python", FileExtensions) + python, ok = holotreePath.Which("python", FileExtensions) } if ok { environment = append(environment, "PYTHON_EXE="+python) @@ -112,16 +119,27 @@ func EnvironmentExtensionFor(location string) []string { "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), + "RCC_VERSION="+common.Version, "TEMP="+common.RobocorpTemp(), "TMP="+common.RobocorpTemp(), FindPath(location).AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) + environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) + environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) + environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) + environment = appendIfValue(environment, "HTTP_PROXY", settings.Global.HttpProxy()) + if settings.Global.HasPipRc() { + environment = appendIfValue(environment, "PIP_CONFIG_FILE", common.PipRcFile()) + } return environment } -func EnvironmentFor(location string) []string { - return append(os.Environ(), EnvironmentExtensionFor(location)...) +func appendIfValue(environment []string, key, value string) []string { + if len(value) > 0 { + return append(environment, key+"="+value) + } + return environment } func AsVersion(incoming string) (uint64, string) { diff --git a/conda/workflows.go b/conda/workflows.go index c730278b..9b2a6f08 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -33,7 +33,7 @@ func livePrepare(liveFolder string, command ...string) (*shell.Task, error) { } common.Debug("Using %v as command %v.", task, commandName) command[0] = task - environment := EnvironmentFor(liveFolder) + environment := CondaExecutionEnvironment(liveFolder, nil, true) return shell.New(environment, ".", command...), nil } diff --git a/docs/changelog.md b/docs/changelog.md index 972637f3..89137c73 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.9.5 (date: 23.3.2022) UNSTABLE + +- refactoring variables exporting into one place +- adding `PIP_CONFIG_FILE`, `HTTP_PROXY`, and `HTTPS_PROXY` variables into + conda environment if they are configured + ## v11.9.4 (date: 22.3.2022) UNSTABLE - profile exporting now works diff --git a/operations/running.go b/operations/running.go index 1331a966..695d6b2e 100644 --- a/operations/running.go +++ b/operations/running.go @@ -217,7 +217,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } task[0] = fullpath directory := config.WorkingDirectory() - environment := config.ExecutionEnvironment(label, developmentEnvironment.AsEnvironment(), true) + environment := config.RobotExecutionEnvironment(label, developmentEnvironment.AsEnvironment(), true) if len(data) > 0 { endpoint := data["endpoint"] for _, key := range rcHosts { diff --git a/robot/robot.go b/robot/robot.go index f6acd177..1cbf11db 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -14,7 +14,6 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/xviper" "github.com/google/shlex" "gopkg.in/yaml.v2" @@ -46,7 +45,7 @@ type Robot interface { Paths() pathlib.PathParts PythonPaths() pathlib.PathParts SearchPath(location string) pathlib.PathParts - ExecutionEnvironment(location string, inject []string, full bool) []string + RobotExecutionEnvironment(location string, inject []string, full bool) []string } type Task interface { @@ -420,45 +419,14 @@ func (it *robot) SearchPath(location string) pathlib.PathParts { return conda.FindPath(location).Prepend(it.Paths()...) } -func (it *robot) ExecutionEnvironment(location string, inject []string, full bool) []string { - environment := make([]string, 0, 100) - if full { - environment = append(environment, os.Environ()...) - } - environment = append(environment, inject...) - holotreePath := conda.HolotreePath(location) - python, ok := holotreePath.Which("python3", conda.FileExtensions) - if !ok { - python, ok = holotreePath.Which("python", conda.FileExtensions) - } - if ok { - environment = append(environment, "PYTHON_EXE="+python) - } - searchPath := it.SearchPath(location) - environment = append(environment, - "CONDA_DEFAULT_ENV=rcc", - "CONDA_PREFIX="+location, - "CONDA_PROMPT_MODIFIER=(rcc) ", - "CONDA_SHLVL=1", - "PYTHONHOME=", - "PYTHONSTARTUP=", - "PYTHONEXECUTABLE=", - "PYTHONNOUSERSITE=1", - "PYTHONDONTWRITEBYTECODE=x", - "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), - "ROBOCORP_HOME="+common.RobocorpHome(), - "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, - "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), - "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), - "TEMP="+common.RobocorpTemp(), - "TMP="+common.RobocorpTemp(), - searchPath.AsEnvironmental("PATH"), +func (it *robot) RobotExecutionEnvironment(location string, inject []string, full bool) []string { + environment := conda.CondaExecutionEnvironment(location, inject, full) + return append(environment, + it.SearchPath(location).AsEnvironmental("PATH"), it.PythonPaths().AsEnvironmental("PYTHONPATH"), fmt.Sprintf("ROBOT_ROOT=%s", it.WorkingDirectory()), fmt.Sprintf("ROBOT_ARTIFACTS=%s", it.ArtifactDirectory()), ) - environment = append(environment, conda.LoadActivationEnvironment(location)...) - return environment } func (it *task) shellCommand() []string { From 894a2323e110c29af0bacfd80b423e851ebf9702 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 25 Mar 2022 14:30:56 +0200 Subject: [PATCH 231/516] WIP: more environment variables (v11.9.6) - adding more setting options and environment variables - added support for CA-bundles in pem format --- assets/settings.yaml | 3 +-- common/variables.go | 4 ++++ common/version.go | 2 +- conda/robocorp.go | 10 +++++++++ docs/changelog.md | 5 +++++ operations/diagnostics.go | 2 ++ settings/api.go | 3 +++ settings/data.go | 3 ++- settings/profile.go | 5 +++++ settings/settings.go | 47 ++++++++++++++++++++------------------- wizard/common.go | 23 ++++++++++++++++--- wizard/config.go | 28 +++++++++++++++++++---- 12 files changed, 101 insertions(+), 34 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index 7e5894c5..242aa1ef 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -24,8 +24,7 @@ autoupdates: certificates: verify-ssl: true - no-revocation: false - custom-cert: # no custom certificate by default + ssl-no-revoke: false network: https-proxy: # no proxy by default diff --git a/common/variables.go b/common/variables.go index fbb1ab31..54b0cc64 100644 --- a/common/variables.go +++ b/common/variables.go @@ -166,6 +166,10 @@ func SettingsFile() string { return ExpandPath(filepath.Join(RobocorpHome(), "settings.yaml")) } +func CaBundleFile() string { + return ExpandPath(filepath.Join(RobocorpHome(), "ca-bundle.pem")) +} + func UnifyVerbosityFlags() { if Silent { DebugFlag = false diff --git a/common/version.go b/common/version.go index 085a8d02..b87ae147 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.5` + Version = `v11.9.6` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index ae20a61c..7f200500 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -125,6 +125,12 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st FindPath(location).AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) + if settings.Global.NoRevocation() { + environment = append(environment, "MAMBA_SSL_NO_REVOKE=true") + } + if settings.Global.VerifySsl() { + environment = append(environment, "MAMBA_SSL_VERIFY=false") + } environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) @@ -132,6 +138,10 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if settings.Global.HasPipRc() { environment = appendIfValue(environment, "PIP_CONFIG_FILE", common.PipRcFile()) } + if settings.Global.HasCaBundle() { + environment = appendIfValue(environment, "REQUESTS_CA_BUNDLE", common.CaBundleFile()) + environment = appendIfValue(environment, "CURL_CA_BUNDLE", common.CaBundleFile()) + } return environment } diff --git a/docs/changelog.md b/docs/changelog.md index 89137c73..517634b3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.9.6 (date: 25.3.2022) UNSTABLE + +- adding more setting options and environment variables +- added support for CA-bundles in pem format + ## v11.9.5 (date: 23.3.2022) UNSTABLE - refactoring variables exporting into one place diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 8ad6761d..364c96c9 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -84,6 +84,8 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["config-active-profile"] = settings.Global.Name() result.Details["config-https-proxy"] = settings.Global.HttpsProxy() result.Details["config-http-proxy"] = settings.Global.HttpProxy() + result.Details["config-ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) + result.Details["config-ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") diff --git a/settings/api.go b/settings/api.go index 49db0bf4..f08af76e 100644 --- a/settings/api.go +++ b/settings/api.go @@ -30,4 +30,7 @@ type Api interface { HttpProxy() string HasPipRc() bool HasMicroMambaRc() bool + HasCaBundle() bool + VerifySsl() bool + NoRevocation() bool } diff --git a/settings/data.go b/settings/data.go index c04307f0..dbb40ccf 100644 --- a/settings/data.go +++ b/settings/data.go @@ -224,7 +224,8 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { } type Certificates struct { - VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` + VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` + SslNoRevoke bool `yaml:"ssl-no-revoke" json:"ssl-no-revoke"` } func (it *Certificates) onTopOf(target *Settings) { diff --git a/settings/profile.go b/settings/profile.go index 4193ae7b..8923eb41 100644 --- a/settings/profile.go +++ b/settings/profile.go @@ -18,6 +18,7 @@ type Profile struct { Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` PipRc string `yaml:"piprc,omitempty" json:"piprc,omitempty"` MicroMambaRc string `yaml:"micromambarc,omitempty" json:"micromambarc,omitempty"` + CaBundle string `yaml:"ca-bundle,omitempty" json:"ca-bundle,omitempty"` } func (it *Profile) AsYaml() ([]byte, error) { @@ -65,6 +66,8 @@ func (it *Profile) Activate() (err error) { fail.On(err != nil, "Failed to save piprc, reason: %v", err) err = saveIfBody(common.MicroMambaRcFile(), []byte(it.MicroMambaRc)) fail.On(err != nil, "Failed to save micromambarc, reason: %v", err) + err = saveIfBody(common.CaBundleFile(), []byte(it.CaBundle)) + fail.On(err != nil, "Failed to save ca-bundle.pem, reason: %v", err) return nil } @@ -77,6 +80,8 @@ func (it *Profile) Remove() (err error) { fail.On(err != nil, "Failed to remove micromambarc, reason: %v", err) err = removeIfExists(common.SettingsFile()) fail.On(err != nil, "Failed to remove settings.yaml, reason: %v", err) + err = removeIfExists(common.CaBundleFile()) + fail.On(err != nil, "Failed to remove ca-bundle.pem, reason: %v", err) return nil } diff --git a/settings/settings.go b/settings/settings.go index 2ae33b2f..97e83012 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -130,34 +130,30 @@ func resolveLink(link, page string) string { type gateway bool -func (it gateway) Name() string { +func (it gateway) settings() *Settings { config, err := SummonSettings() pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Meta.Name + return config +} + +func (it gateway) Name() string { + return it.settings().Meta.Name } func (it gateway) Description() string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Meta.Description + return it.settings().Meta.Description } func (it gateway) TemplatesYamlURL() string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Autoupdates["templates"] + return it.settings().Autoupdates["templates"] } func (it gateway) Diagnostics(target *common.DiagnosticStatus) { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - config.Diagnostics(target) + it.settings().Diagnostics(target) } func (it gateway) Endpoint(key string) string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Endpoints[key] + return it.settings().Endpoints[key] } func (it gateway) DefaultEndpoint() string { @@ -185,15 +181,11 @@ func (it gateway) CondaURL() string { } func (it gateway) HttpsProxy() string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Network.HttpsProxy + return it.settings().Network.HttpsProxy } func (it gateway) HttpProxy() string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Network.HttpProxy + return it.settings().Network.HttpProxy } func (it gateway) HasPipRc() bool { return pathlib.IsFile(common.PipRcFile()) @@ -203,6 +195,10 @@ func (it gateway) HasMicroMambaRc() bool { return pathlib.IsFile(common.MicroMambaRcFile()) } +func (it gateway) HasCaBundle() bool { + return pathlib.IsFile(common.CaBundleFile()) +} + func (it gateway) DownloadsLink(resource string) string { return resolveLink(it.Endpoint("downloads"), resource) } @@ -228,9 +224,14 @@ func (it gateway) CondaLink(page string) string { } func (it gateway) Hostnames() []string { - config, err := SummonSettings() - pretty.Guard(err == nil, 111, "Could not get settings, reason: %v", err) - return config.Hostnames() + return it.settings().Hostnames() +} +func (it gateway) VerifySsl() bool { + return it.settings().Certificates.VerifySsl +} + +func (it gateway) NoRevocation() bool { + return it.settings().Certificates.SslNoRevoke } func (it gateway) ConfiguredHttpTransport() *http.Transport { diff --git a/wizard/common.go b/wizard/common.go index c0d7e86f..45cb985c 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -11,6 +11,11 @@ import ( "github.com/robocorp/rcc/pretty" ) +const ( + UNIX_NEWLINE = "\n" + WINDOWS_NEWLINE = "\r\n" +) + var ( namePattern = regexp.MustCompile("^[\\w-]*$") ) @@ -19,6 +24,18 @@ type Validator func(string) bool type WizardFn func([]string) error +func memberValidation(members []string, erratic string) Validator { + return func(input string) bool { + for _, member := range members { + if input == member { + return true + } + } + common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) + return false + } +} + func regexpValidation(validator *regexp.Regexp, erratic string) Validator { return func(input string) bool { if !validator.MatchString(input) { @@ -68,13 +85,13 @@ func ask(question, defaults string, validator Validator) (string, error) { if err != nil { return "", err } + if reply == UNIX_NEWLINE || reply == WINDOWS_NEWLINE { + reply = defaults + } reply = strings.TrimSpace(reply) if !validator(reply) { continue } - if len(reply) == 0 { - return defaults, nil - } return reply, nil } } diff --git a/wizard/config.go b/wizard/config.go index 65076810..d122a663 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -46,6 +46,9 @@ func Configure(arguments []string) error { common.Stdout("\n") note("You are now configuring a profile to be used in Robocorp toolchain.\n") + note("If you want to clear some value, try giving just one space as a value.\n") + note("If you want to use default value, just press enter.\n") + answers := make(answers) warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") @@ -62,14 +65,19 @@ func Configure(arguments []string) error { answers["profile-description"] = settings.Global.Description() answers["https-proxy"] = settings.Global.HttpsProxy() answers["http-proxy"] = settings.Global.HttpProxy() + answers["ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) + answers["ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) err = questionaire(questions{ {"profile-name", "Give profile a name", regexpValidation(namePattern, "Use just normal english word characters and no spaces!")}, {"profile-description", "Give a short description of this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, - {"micromamba-rc", "Path to micromambarc file", optionalFileValidation("Value should be valid file in filesystem.")}, - {"pip-rc", "Path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"ssl-verify", "Verify SSL certificated (ssl-verify)", memberValidation([]string{"true", "false"}, "Must be either true or false")}, + {"ssl-no-revoke", "Do not check SSL revocations (ssl-no-revoke)", memberValidation([]string{"true", "false"}, "Must be either true or false")}, + {"micromamba-rc", "Optional path to micromambarc file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"pip-rc", "Optional path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, + {"ca-bundle", "Optional path to CA bundle [pem format] file", optionalFileValidation("Value should be valid file in filesystem.")}, }, answers) if err != nil { return err @@ -93,6 +101,11 @@ func Configure(arguments []string) error { HttpProxy: answers["http-proxy"], } + profile.Settings.Certificates = &settings.Certificates{ + VerifySsl: answers["ssl-verify"] == "true", + SslNoRevoke: answers["ssl-no-revoke"] == "true", + } + profile.Settings.Meta.Name = name profile.Settings.Meta.Description = answers["profile-description"] @@ -106,8 +119,15 @@ func Configure(arguments []string) error { profile.PipRc = string(blob) } - // FIXME: following is just temporary "work in progress" save - profile.SaveAs(fmt.Sprintf("profile_%s.yaml", strings.ToLower(name))) + blob, ok = pullFile(answers["ca-bundle"]) + if ok { + profile.CaBundle = string(blob) + } + + profilename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) + profile.SaveAs(profilename) + + note(fmt.Sprintf("Saved profile into file %q.", profilename)) return nil } From e9f0871db126a3f6518369aa6c9d5005e0c2c949 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 28 Mar 2022 14:47:21 +0300 Subject: [PATCH 232/516] Feature: multiple profile support (v11.9.7) - profiles should now be good enough to start testing them - interactive configuration now has instructions for next steps (kind of scripted but not automated; copy-paste instructions) - added placeholder `docs/profile_configuration.md` for future documentation - settings.yaml now has Automation Studio autoupdate URL - added `robot_tests/profiles.robot` to test new functionality --- assets/settings.yaml | 1 + cmd/wizardconfig.go | 5 --- common/version.go | 2 +- docs/changelog.md | 9 +++++ docs/profile_configuration.md | 3 ++ robot_tests/bug_reports.robot | 1 - robot_tests/profile_alpha.yaml | 14 +++++++ robot_tests/profile_beta.yaml | 14 +++++++ robot_tests/profiles.robot | 73 ++++++++++++++++++++++++++++++++++ settings/settings.go | 40 ++++++++++++++++++- wizard/common.go | 4 +- wizard/config.go | 13 +++++- 12 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 docs/profile_configuration.md create mode 100644 robot_tests/profile_alpha.yaml create mode 100644 robot_tests/profile_beta.yaml create mode 100644 robot_tests/profiles.robot diff --git a/assets/settings.yaml b/assets/settings.yaml index 242aa1ef..6077c26d 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -18,6 +18,7 @@ diagnostics-hosts: autoupdates: assistant: https://downloads.robocorp.com/assistant/releases/ + automation-studio: https://downloads.robocorp.com/automation-studio/releases/ workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ lab: https://downloads.robocorp.com/lab/releases/ templates: https://downloads.robocorp.com/templates/templates.yaml diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go index 32908cd6..ec093bff 100644 --- a/cmd/wizardconfig.go +++ b/cmd/wizardconfig.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/wizard" @@ -25,10 +24,6 @@ var wizardConfigCommand = &cobra.Command{ if err != nil { pretty.Exit(2, "%v", err) } - _, err = operations.ProduceDiagnostics("", "", false, true) - if err != nil { - pretty.Exit(3, "Error: %v", err) - } pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index b87ae147..00fce3ef 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.6` + Version = `v11.9.7` ) diff --git a/docs/changelog.md b/docs/changelog.md index 517634b3..9b446867 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.9.7 (date: 28.3.2022) + +- profiles should now be good enough to start testing them +- interactive configuration now has instructions for next steps (kind of + scripted but not automated; copy-paste instructions) +- added placeholder `docs/profile_configuration.md` for future documentation +- settings.yaml now has Automation Studio autoupdate URL +- added `robot_tests/profiles.robot` to test new functionality + ## v11.9.6 (date: 25.3.2022) UNSTABLE - adding more setting options and environment variables diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md new file mode 100644 index 00000000..60794469 --- /dev/null +++ b/docs/profile_configuration.md @@ -0,0 +1,3 @@ +# Profile Configuration + +To be written. diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index 10fe50f8..fa156046 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -11,7 +11,6 @@ Github issue 7 about initial call with do-not-track Must Have anonymous health tracking is: disabled Bug in virtual holotree with gzipped files - [Tags] WIP Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml Use STDERR Must Have Blueprint "ef0163b57ff44cd5" is available: false diff --git a/robot_tests/profile_alpha.yaml b/robot_tests/profile_alpha.yaml new file mode 100644 index 00000000..8c8b26d4 --- /dev/null +++ b/robot_tests/profile_alpha.yaml @@ -0,0 +1,14 @@ +name: Alpha +description: Alpha settings +settings: + certificates: + verify-ssl: true + ssl-no-revoke: false + network: + https-proxy: "" + http-proxy: "" + meta: + name: Alpha + description: Alpha settings + source: tests + version: 1.2.3.4 diff --git a/robot_tests/profile_beta.yaml b/robot_tests/profile_beta.yaml new file mode 100644 index 00000000..9fe81e92 --- /dev/null +++ b/robot_tests/profile_beta.yaml @@ -0,0 +1,14 @@ +name: Beta +description: Beta settings +settings: + certificates: + verify-ssl: false + ssl-no-revoke: true + network: + https-proxy: http://beta.net:1234/ + http-proxy: http://beta.net:2345/ + meta: + name: Beta + description: Beta settings + source: tests + version: 1.2.3.4 diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot new file mode 100644 index 00000000..290ed862 --- /dev/null +++ b/robot_tests/profiles.robot @@ -0,0 +1,73 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot +Default Tags WIP + +*** Test cases *** + +Goal: Initially there are no profiles + Step build/rcc configuration switch 2 + Use STDERR + Must Have No profiles found, you must first import some. + +Goal: Can import profiles into rcc + Step build/rcc configuration import --filename robot_tests/profile_alpha.yaml + Use STDERR + Must Have OK. + + Step build/rcc configuration import --filename robot_tests/profile_beta.yaml + Use STDERR + Must Have OK. + +Goal: Can see imported profiles + Step build/rcc configuration switch + Must Have Alpha settings + Must Have Beta settings + Must Have Currently active profile is: default + Use STDERR + Must Have OK. + +Goal: Can see imported profiles as json + Step build/rcc configuration switch --json + Must Be Json Response + Must Have "Alpha settings" + Must Have "Beta settings" + +Goal: Can switch to Alpha profile + Step build/rcc configuration switch --profile alpha + Use STDERR + Must Have OK. + + Step build/rcc configuration switch + Use STDOUT + Must Have Currently active profile is: Alpha + Use STDERR + Must Have OK. + +Goal: Can switch to Beta profile + Step build/rcc configuration switch --profile Beta + Use STDERR + Must Have OK. + + Step build/rcc configuration switch + Use STDOUT + Must Have Currently active profile is: Beta + Use STDERR + Must Have OK. + +Goal: Can switch to no profile + Step build/rcc configuration switch --noprofile + Use STDERR + Must Have OK. + + Step build/rcc configuration switch + Use STDOUT + Must Have Currently active profile is: default + Use STDERR + Must Have OK. + +Goal: Can export profile + Step build/rcc configuration export --profile Alpha --filename tmp/exported_alpha.yaml + Use STDERR + Must Have OK. diff --git a/settings/settings.go b/settings/settings.go index 97e83012..6f433cb5 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "crypto/tls" + "crypto/x509" "fmt" "io" "io/ioutil" @@ -238,6 +239,25 @@ func (it gateway) ConfiguredHttpTransport() *http.Transport { return httpTransport } +func (it gateway) loadRootCAs() *x509.CertPool { + if !it.HasCaBundle() { + return nil + } + certificates, err := os.ReadFile(common.CaBundleFile()) + if err != nil { + common.Log("Warning! Problem reading %q, reason: %v", common.CaBundleFile(), err) + return nil + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(certificates) + if !ok { + common.Log("Warning! Problem appending sertificated from %q.", common.CaBundleFile()) + return nil + } + return roots +} + func init() { chain = SettingsLayers{ DefaultSettingsLayer(), @@ -251,7 +271,23 @@ func init() { if err == nil && settings.Certificates != nil { verifySsl = settings.Certificates.VerifySsl } - if !verifySsl { - httpTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + proxyUrl := "" + if len(Global.HttpProxy()) > 0 { + proxyUrl = Global.HttpProxy() + } + if len(Global.HttpsProxy()) > 0 { + proxyUrl = Global.HttpsProxy() + } + if len(proxyUrl) > 0 { + link, err := url.Parse(proxyUrl) + if err != nil { + common.Log("Warning! Problem parsing proxy URL %q, reason: %v.", proxyUrl, err) + } else { + httpTransport.Proxy = http.ProxyURL(link) + } + } + httpTransport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: !verifySsl, + RootCAs: Global.loadRootCAs(), } } diff --git a/wizard/common.go b/wizard/common.go index 45cb985c..1daf117f 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -2,6 +2,7 @@ package wizard import ( "bufio" + "fmt" "os" "regexp" "strings" @@ -72,7 +73,8 @@ func firstOf(arguments []string, missing string) string { return missing } -func note(message string) { +func note(form string, details ...interface{}) { + message := fmt.Sprintf(form, details...) common.Stdout("%s! %s%s%s\n", pretty.Red, pretty.White, message, pretty.Reset) } diff --git a/wizard/config.go b/wizard/config.go index d122a663..edafe2c2 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" ) @@ -45,8 +46,9 @@ func questionaire(questions questions, answers answers) error { func Configure(arguments []string) error { common.Stdout("\n") + note("Read documentation at %shttps://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md%s\n", pretty.Green, pretty.White) note("You are now configuring a profile to be used in Robocorp toolchain.\n") - note("If you want to clear some value, try giving just one space as a value.\n") + note("If you want to clear some value, try giving just one space as a value.") note("If you want to use default value, just press enter.\n") answers := make(answers) @@ -127,7 +129,14 @@ func Configure(arguments []string) error { profilename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) profile.SaveAs(profilename) - note(fmt.Sprintf("Saved profile into file %q.", profilename)) + note("Saved profile into file %q.\n", profilename) + note("Next steps:") + note(" 1. command `%srcc configuration import --filename %s%s` imports created profile.", pretty.Grey, profilename, pretty.White) + note(" 2. command `%srcc configuration switch --profile %s%s` switches to that profile.", pretty.Grey, name, pretty.White) + note(" 3. command `%srcc configuration diagnostics%s` shows diagnostics with that configuration.", pretty.Grey, pretty.White) + note(" 4. command `%srcc configuration speedtest%s` can verify full environment creation.", pretty.Grey, pretty.White) + note(" 5. command `%srcc interactive configuration %s%s` runs this interactive command again.", pretty.Grey, name, pretty.White) + note(" 6. command `%srcc configuration switch --noprofile%s` inactivates all profiles.\n", pretty.Grey, pretty.White) return nil } From b843e6327be36cc01cce2425d52f35f225ea2fef Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 29 Mar 2022 13:26:10 +0300 Subject: [PATCH 233/516] Profile testing and documentation (v11.9.8) - updated profile documentation - added integrity check on hololib to space extraction - more robot tests added - fixed ssl-no-revoke bug (found thru new robot tests) --- common/version.go | 2 +- docs/changelog.md | 7 ++++++ docs/profile_configuration.md | 44 ++++++++++++++++++++++++++++++++++- htfs/functions.go | 11 ++++++++- robot_tests/profile_beta.yaml | 9 +++++-- robot_tests/profiles.robot | 22 ++++++++++++++++++ settings/data.go | 1 + 7 files changed, 91 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 00fce3ef..be5a69f1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.7` + Version = `v11.9.8` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9b446867..6a68767b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.9.8 (date: 29.3.2022) + +- updated profile documentation +- added integrity check on hololib to space extraction +- more robot tests added +- fixed ssl-no-revoke bug (found thru new robot tests) + ## v11.9.7 (date: 28.3.2022) - profiles should now be good enough to start testing them diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md index 60794469..2ac1f46b 100644 --- a/docs/profile_configuration.md +++ b/docs/profile_configuration.md @@ -1,3 +1,45 @@ # Profile Configuration -To be written. +## Quick start guide + +```sh +# interactively create "Office" profile +rcc interactive configuration Office + +# import that Office profile, so that it can be used +rcc configuration import --filename profile_office.yaml + +# start using that Office profile +rcc configuration switch --profile Office + +# verify that basic things work by doing diagnostics +rcc configuration diagnostics + +# when basics work, see if full environment creation works +rcc configuration speedtest + +# when you want to reset profile to "default" state +rcc configuration switch --noprofile + +# if you want to export profile and deliver to others +rcc configuration export --profile Office --filename shared.yaml +``` + +## What is needed? + +- you need rcc 11.9.7 or later +- your existing `settings.yaml` file (optional) +- your existing `micromambarc` file (optional) +- your existing `pip.ini` file (optional) +- your existing `cabundle.pem` file (optional) +- knowledge about your network proxies and certificate policies + +## Discovery process + +1. You must be inside that network that you are targetting the configuration. +2. Run interactive configuration and answer questions there. +3. Take created profile in use. +4. Run diagnostics and speed test to verify functionality. +5. Repeat these steps until everything works. +6. Export profile and share it with rest of your team/organization. +7. Create other profiles for different network locations (remote, VPN, ...) diff --git a/htfs/functions.go b/htfs/functions.go index 11f29673..89525e3a 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -278,9 +278,18 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ sink, err := os.Create(partname) anywork.OnErrPanicCloseAll(err) - _, err = io.Copy(sink, reader) + digester := sha256.New() + many := io.MultiWriter(sink, digester) + + _, err = io.Copy(many, reader) anywork.OnErrPanicCloseAll(err, sink) + hexdigest := fmt.Sprintf("%02x", digester.Sum(nil)) + if digest != hexdigest { + err := fmt.Errorf("Corrupted hololib, expected %s, actual %s", digest, hexdigest) + anywork.OnErrPanicCloseAll(err, sink) + } + for _, position := range details.Rewrite { _, err = sink.Seek(position, 0) if err != nil { diff --git a/robot_tests/profile_beta.yaml b/robot_tests/profile_beta.yaml index 9fe81e92..7af7f5fe 100644 --- a/robot_tests/profile_beta.yaml +++ b/robot_tests/profile_beta.yaml @@ -5,10 +5,15 @@ settings: verify-ssl: false ssl-no-revoke: true network: - https-proxy: http://beta.net:1234/ - http-proxy: http://beta.net:2345/ + https-proxy: http://bad.betaputkinen.net:1234/ + http-proxy: http://bad.betaputkinen.net:2345/ meta: name: Beta description: Beta settings source: tests version: 1.2.3.4 +piprc: | + [global] + trusted-host = pypi.python.org pypi.org files.pythonhosted.org www.googleapis.com api.github.com selenium-release.storage.googleapis.com +micromambarc: | + ssl_verify: False diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index 290ed862..30e6c409 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -45,6 +45,17 @@ Goal: Can switch to Alpha profile Use STDERR Must Have OK. +Goal: Diagnostics can show alpha profile information + Step build/rcc configuration diagnostics --json + Must Be Json Response + Must Have "config-micromambarc-used": "false" + Must Have "config-piprc-used": "false" + Must Have "config-settings-yaml-used": "true" + Must Have "config-ssl-no-revoke": "false" + Must Have "config-ssl-verify": "true" + Must Have "config-https-proxy": "" + Must Have "config-http-proxy": "" + Goal: Can switch to Beta profile Step build/rcc configuration switch --profile Beta Use STDERR @@ -56,6 +67,17 @@ Goal: Can switch to Beta profile Use STDERR Must Have OK. +Goal: Diagnostics can show beta profile information + Step build/rcc configuration diagnostics --json + Must Be Json Response + Must Have "config-micromambarc-used": "true" + Must Have "config-piprc-used": "true" + Must Have "config-settings-yaml-used": "true" + Must Have "config-ssl-no-revoke": "true" + Must Have "config-ssl-verify": "false" + Must Have "config-https-proxy": "http://bad.betaputkinen.net:1234/" + Must Have "config-http-proxy": "http://bad.betaputkinen.net:2345/" + Goal: Can switch to no profile Step build/rcc configuration switch --noprofile Use STDERR diff --git a/settings/data.go b/settings/data.go index dbb40ccf..b0179ac1 100644 --- a/settings/data.go +++ b/settings/data.go @@ -233,6 +233,7 @@ func (it *Certificates) onTopOf(target *Settings) { target.Certificates = &Certificates{} } target.Certificates.VerifySsl = it.VerifySsl + target.Certificates.SslNoRevoke = it.SslNoRevoke } type Endpoints struct { From f616a56e5909d9d6a3fdda15a345fe6567cfe819 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 31 Mar 2022 12:19:18 +0300 Subject: [PATCH 234/516] Documentation and message fixes (v11.9.9) - updated interactive create with `--task` option alternative - updated run error message with `--task` option instructions - this closes #28 - updated `recipes.md` with python conversion instructions --- common/version.go | 2 +- docs/changelog.md | 7 +++++++ docs/recipes.md | 49 ++++++++++++++++++++++++++++++++++++++++--- operations/running.go | 2 +- robot/robot.go | 2 +- wizard/create.go | 1 + 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index be5a69f1..2d779fdc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.8` + Version = `v11.9.9` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6a68767b..6fe16c20 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.9.9 (date: 31.3.2022) + +- updated interactive create with `--task` option alternative +- updated run error message with `--task` option instructions +- this closes #28 +- updated `recipes.md` with python conversion instructions + ## v11.9.8 (date: 29.3.2022) - updated profile documentation diff --git a/docs/recipes.md b/docs/recipes.md index 9a1caea2..6ea81524 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -128,6 +128,45 @@ rcc task script --silent -- pip list rcc task script --interactive -- ipython ``` +## How to convert existing python project to rcc? + +### Basic workflow to get it up and running + +1. Create a new robot using `rcc create` with `Basic Python template`. +2. Remove task.py and and copy files from your existing project to this new + rcc/robot project. +3. Discover all your publicly available dependencies (including your python + version) and try find as many as possible from https://anaconda.org/conda-forge/ + and take rest from https://pypi.org/ and put those dependencies + into `conda.yaml`. And remove all those dependencies that you do not actually + need in your project. +4. Do not add any private dependencies into `conda.yaml`, and also no passwords + in that `conda.yaml` either (passwords belong to secure place, like Vault). +5. Modify your `robot.yaml` task definitions so, that it is how your python + project should be executed. +6. If you have additional private libraries, put them inside robot directory + structure (like under `libraries` or something similar) and edit PYTHONPATH + settings in `robot.yaml` to include those paths (relative paths only). +7. If you have additional scripts/small binaries that your robot dependes on, + add them inside robot directory structure (like under `scripts` directory) + and edit PATH settings in `robot.yaml` to include that (relative) path. +8. If your python project needs external dependencies (like Word or Excel) + then those dependencies must be present in machine where robot is executed + and they are not part of this conversion. +9. Run robot and test if it works, and iterate to make needed changes. + +### What next? + +* Your python project is now converted to rcc and should be locally "runnable". +* Setup Assistant or Workforce Agent in your machine and create Assistant or + Robot in Robocorp Control Room, and try to run it from there. +* If your robot is "headless", has all dependencies, and should be runnable + in Linux, then you can try to run it in container from Control Room. +* If your project is python2 project, then consider converting it to python3. +* If you want to use `rpaframework` in your robot (like dialogs for example), + then you have to start converting to use those features in your code. +* etc. + ## Is rcc limited to Python and Robot Framework? Absolutely not! Here is something completely different for you to think about. @@ -272,20 +311,24 @@ See: https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md On Linux/MacOSX: -``` +```sh +# full robot environment source <(rcc holotree variables --space mine --robot path/to/robot.yaml) + +# or with just conda.yaml +source <(rcc holotree variables --space mine path/to/conda.yaml) ``` On Windows -``` +```sh rcc holotree variables --space mine --robot path/to/robot.yaml > mine_activate.bat call mine_activate.bat ``` You can also try -``` +```sh rcc task shell --robot path/to/robot.yaml ``` diff --git a/operations/running.go b/operations/running.go index 695d6b2e..715edead 100644 --- a/operations/running.go +++ b/operations/running.go @@ -104,7 +104,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. todo := config.TaskByName(theTask) if todo == nil { - pretty.Exit(3, "Error: Could not resolve task to run. Available tasks are: %v", strings.Join(config.AvailableTasks(), ", ")) + pretty.Exit(3, "Error: Could not resolve what task to run. Select one using --task option.\nAvailable task names are: %v.", strings.Join(config.AvailableTasks(), ", ")) } if config.HasHolozip() && !common.UsesHolotree() { diff --git a/robot/robot.go b/robot/robot.go index 1cbf11db..60da7cfc 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -283,7 +283,7 @@ func (it *robot) IgnoreFiles() []string { func (it *robot) AvailableTasks() []string { result := make([]string, 0, len(it.Tasks)) for name, _ := range it.Tasks { - result = append(result, name) + result = append(result, fmt.Sprintf("%q", name)) } sort.Strings(result) return result diff --git a/wizard/create.go b/wizard/create.go index 3857de27..26079b1a 100644 --- a/wizard/create.go +++ b/wizard/create.go @@ -92,6 +92,7 @@ func Create(arguments []string) error { common.Stdout("%s$ %scd %s%s\n", pretty.Grey, pretty.Cyan, robotName, pretty.Reset) common.Stdout("%s$ %srcc run%s\n", pretty.Grey, pretty.Cyan, pretty.Reset) + common.Stdout("%s# or with name in case of multiple tasks in one robot\n$ %srcc run --task \"\"%s\n", pretty.Grey, pretty.Cyan, pretty.Reset) common.Stdout("\n") return nil From 9104dfe6e444efbef0011714c8f51d35565c28e9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Apr 2022 11:53:52 +0300 Subject: [PATCH 235/516] Documentation and message fixes (v11.9.10) - added current profile in JSON response from configuration switch command - fixing bugs and typos in code and texts --- cmd/configureswitch.go | 11 +++++++---- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot_tests/profiles.robot | 2 ++ wizard/config.go | 5 +++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go index cd5fbfe1..1f25dc81 100644 --- a/cmd/configureswitch.go +++ b/cmd/configureswitch.go @@ -17,19 +17,22 @@ func profileMap() map[string]string { pattern := common.ExpandPath(filepath.Join(common.RobocorpHome(), "profile_*.yaml")) found, err := filepath.Glob(pattern) pretty.Guard(err == nil, 1, "Error while searching profiles: %v", err) - result := make(map[string]string) + profiles := make(map[string]string) for _, name := range found { profile := settings.Profile{} err = profile.LoadFrom(name) if err == nil { - result[profile.Name] = profile.Description + profiles[profile.Name] = profile.Description } } - return result + return profiles } func jsonListProfiles() { - content, err := operations.NiceJsonOutput(profileMap()) + profiles := make(map[string]interface{}) + profiles["profiles"] = profileMap() + profiles["current"] = settings.Global.Name() + content, err := operations.NiceJsonOutput(profiles) pretty.Guard(err == nil, 1, "Error serializing profiles: %v", err) common.Stdout("%s\n", content) } diff --git a/common/version.go b/common/version.go index 2d779fdc..d5098911 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.9` + Version = `v11.9.10` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6fe16c20..107d23ca 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.9.10 (date: 4.4.2022) + +- added current profile in JSON response from configuration switch command +- fixing bugs and typos in code and texts + ## v11.9.9 (date: 31.3.2022) - updated interactive create with `--task` option alternative diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index 30e6c409..697d744b 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -31,6 +31,8 @@ Goal: Can see imported profiles Goal: Can see imported profiles as json Step build/rcc configuration switch --json Must Be Json Response + Must Have "current" + Must Have "profiles" Must Have "Alpha settings" Must Have "Beta settings" diff --git a/wizard/config.go b/wizard/config.go index edafe2c2..ccc7c136 100644 --- a/wizard/config.go +++ b/wizard/config.go @@ -55,12 +55,13 @@ func Configure(arguments []string) error { warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - filename, err := ask("Path to (otional) settings.yaml", "", optionalFileValidation("Value should be valid file in filesystem.")) + filename, err := ask("Path to (optional) settings.yaml", "", optionalFileValidation("Value should be valid file in filesystem.")) if err != nil { return err } if len(filename) > 0 { settings.TemporalSettingsLayer(filename) + answers["settings-yaml"] = filename } answers["profile-name"] = firstOf(arguments, settings.Global.Name()) @@ -72,7 +73,7 @@ func Configure(arguments []string) error { err = questionaire(questions{ {"profile-name", "Give profile a name", regexpValidation(namePattern, "Use just normal english word characters and no spaces!")}, - {"profile-description", "Give a short description of this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, + {"profile-description", "Give a short description for this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, {"ssl-verify", "Verify SSL certificated (ssl-verify)", memberValidation([]string{"true", "false"}, "Must be either true or false")}, From 26062cdc09edc68edeb5e573342cc0a251f6eca6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Apr 2022 14:08:17 +0300 Subject: [PATCH 236/516] Documentation updates (v11.9.11) --- common/version.go | 2 +- docs/changelog.md | 4 + docs/profile_configuration.md | 24 +++- docs/recipes.md | 238 ++++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index d5098911..e3451198 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.10` + Version = `v11.9.11` ) diff --git a/docs/changelog.md b/docs/changelog.md index 107d23ca..c8387749 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.9.11 (date: 5.4.2022) + +- documentation updates + ## v11.9.10 (date: 4.4.2022) - added current profile in JSON response from configuration switch command diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md index 2ac1f46b..bc47a4ab 100644 --- a/docs/profile_configuration.md +++ b/docs/profile_configuration.md @@ -1,5 +1,27 @@ # Profile Configuration +## What is profile? + +Profile is way to capture configuration information related to specific +network location. System can have multiple profiles defined, but only one +can be active at any moment. + +### When do you need profiles? + +- if you are in restricted network where direct network access is not available +- if you are working in multiple locations with different access policies + (for example switching between office, hotel, airport, or remote locations) +- if you want to share your working setup with others in same network + +### What does it contain? + +- information from `settings.yaml` (can be partial) +- configuration for micromamba ("micromambarc" is almost like "condarc") +- configuration for pip (pip.ini or piprc) +- root certificate bundle in pem format +- proxy settings (`HTTP_PROXY` and `HTTPS_PROXY`) +- options for `ssl-verify` and `ssl-no-revoke` + ## Quick start guide ```sh @@ -27,7 +49,7 @@ rcc configuration export --profile Office --filename shared.yaml ## What is needed? -- you need rcc 11.9.7 or later +- you need rcc 11.9.10 or later - your existing `settings.yaml` file (optional) - your existing `micromambarc` file (optional) - your existing `pip.ini` file (optional) diff --git a/docs/recipes.md b/docs/recipes.md index 6ea81524..284b56f7 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -1,5 +1,6 @@ # Tips, tricks, and recipies + ## How to see dependency changes? Since version 10.2.2, rcc can show dependency listings using @@ -42,6 +43,7 @@ rcc robot dependencies --space user --export rcc robot dependencies --space user ``` + ## How to freeze dependencies? Starting from rcc 10.3.2, there is now possibility to freeze dependencies. @@ -75,6 +77,7 @@ This is how you can experiment with it. - for better visibility on configuration drift, you should also have `dependencies.yaml` inside your robot (see other recipe for it) + ## How pass arguments to robot from CLI? Since version 9.15.0, rcc supports passing arguments from CLI to underlying @@ -107,6 +110,7 @@ ignoreFiles: rcc task run --interactive --task scripting -- --loglevel TRACE --variable answer:42 tasks.robot ``` + ## How to run any command inside robot environment? Since version 9.20.0, rcc now supports running any command inside robot space @@ -128,6 +132,7 @@ rcc task script --silent -- pip list rcc task script --interactive -- ipython ``` + ## How to convert existing python project to rcc? ### Basic workflow to get it up and running @@ -167,6 +172,7 @@ rcc task script --interactive -- ipython then you have to start converting to use those features in your code. * etc. + ## Is rcc limited to Python and Robot Framework? Absolutely not! Here is something completely different for you to think about. @@ -259,6 +265,7 @@ mkdir -p output cp target/build/micromamba output/micromamba-$version ``` + ## How to control holotree environments? There is three controlling factors for where holotree spaces are created. @@ -332,6 +339,7 @@ You can also try rcc task shell --robot path/to/robot.yaml ``` + ## What can be controlled using environment variables? - `ROBOCORP_HOME` points to directory where rcc keeps most of Robocorp related @@ -344,6 +352,7 @@ rcc task shell --robot path/to/robot.yaml - `RCC_CREDENTIALS_ID` is way to provide Control Room credentials using environment variables + ## How to troubleshoot rcc setup and robots? ```sh @@ -363,6 +372,233 @@ rcc configure speedtest - generic flag `--trace` shows more verbose debugging messages during execution - flag `--timeline` can be used to see execution timeline and where time was spent + +## What is in `robot.yaml`? + +### Example + +```yaml +tasks: + Just a task: + robotTaskName: Just a task + Version command: + shell: python -m robot --version + Multiline command: + command: + - python + - -m + - robot + - --report + - NONE + - -d + - output + - --logtitle + - Task log + - tasks.robot + +condaConfigFile: conda.yaml + +environmentConfigs: +- environment_linux_amd64_freeze.yaml +- environment_windows_amd64_freeze.yaml +- common_linux_amd64.yaml +- common_windows_amd64.yaml +- common_linux.yaml +- common_windows.yaml +- conda.yaml + +preRunScripts: +- privatePipInstall.sh +- initializeKeystore.sh + +artifactsDir: output + +ignoreFiles: +- .gitignore + +PATH: +- . +- bin + +PYTHONPATH: +- . +- libraries +``` + +### What is this `robot.yaml` thing? + +It is declarative description in [YAML format](https://en.wikipedia.org/wiki/YAML) +of what robot is and what it can do. + +It is also a pointer to "a robot center of universe" for directory it resides. +So it is marker of "current working folder" when robot starts to execute and +that will be indicated in `ROBOT_ROOT` environment variable. All declarations +inside `robot.yaml` should be relative to this location, so do not use +absolute paths here. + +Also note that `robot.yaml` is just a name of a file. Other names can be used +and then given to commands using `--robot othername.yaml` CLI option. + +### What are `tasks:`? + +One robot can do multiple tasks. Each task is a single declaration of named +task that robot can do. + +There are three types of task declarations: + +1. The `robotTaskName` form, which is simplest and there only name of a task + is given. In above example `Just a task` is a such thing. This is Robot + Framework specific form. +2. The `shell` form, where full CLI command is given as oneliner. In above + example, `Version command` is example of this. +3. The `command` form is oldest. It is given as list of command and its + arguments, and it is most accurate way to declare CLI form, but it is also + most spacious form. + +### What is `condaConfigFile:`? + +This is actual name used as `conda.yaml` environment configuration file. +See next topic about details of `conda.yaml` file. +This is just single file that describes dependencies for all operating systems. +For more versatile selection, see `environmentConfigs` below. If that +`environmentConfigs` exists and one of those files matches machine running +rcc, then this config is ignored. + +### What are `environmentConfigs:`? + +These are like condaConfigFile above, but as priority list form. First matching +and existing item from that list is used as environment configuration file. + +These files are matched by operating system (windows/darwin/linux) and by +architecture (amd64/arm64). If filename contains word "freeze", it must +match OS and architecture exactly. Other variations allow just some or none +of those parts. + +And if there is no such file, then those entries are just ignored. And if +none of files match or exist, then as final resort, `condaConfigFile` value +is used if present. + +### What are `preRunScripts:`? + +This is set of scripts or commands that are run before actual robot task +execution. Idea with these scripts is that they can be used to customize +runtime environment right after it has been restored from hololib, and just +before actual robot execution is done. + +All these scripts are run in "robot" context with all same environment +variables available as in robot run. + +These scripts can pollute the environment, and it is ok. Next rcc operation +on same holotree space will first do the cleanup though. + +All scripts must be executed successfully or otherwise full robot run is +considered failure and not even tried. Scripts should use exit code zero +to indicate success and everything else is failure. + +Some ideas for scripts could be: + +- install custom packages from private pip repository +- use Vault secrets to prepare system for actual robot run +- setup and customize used tools with secret or other private details that + should not be visible inside hololib catalogs (public caches etc) + +### What is `artifactsDir:`? + +This is location of technical artifacts, like log and freezefiles, that are +created during robot execution and which can be used to find out technical +details about run afterwards. Do not confuse these with work-item data, which +are more business related and do not belong here. + +During robot run, this locations is available using `ROBOT_ARTIFACTS` +environment variable, if you want to store some additional artifacts there. + +### What are `ignoreFiles:`? + +These files are patterns of file and directory names that should not be +stored insided wrapped robots (robot.zip files). Patterns are like git +ignore patterns but less powerful. + +### What are `PATH:`? + +This allows adding entries into `PATH` environment variable. Intention +is to allow something like `bin` directory inside robot, where custom +scripts and binaries can be located and available for execution during +robot run. + +### What are `PYTHONPATH:`? + +This allows adding entries into `PYTHONPATH` environment variable. Intention +is to allow something like `libraries` directory inside robot, where custom +libraries can be located and automatically loaded by python and robot. + + +## What is in `conda.yaml`? + +### Example + +```yaml +channels: +- conda-forge + +dependencies: +- python=3.7.5 +- nodejs=16.14.2 +- pip=20.1 +- pip: + - robotframework-browser==12.3.0 + - rpaframework==13.0.0 + +rccPostInstall: + - rfbrowser init +``` + +### What is this `conda.yaml` thing? + +It is declarative description in [YAML format](https://en.wikipedia.org/wiki/YAML) +of environment that should be set up. + +### What are `channels:`? + +Channels are conda sources where to get packages to be used in setting up +environment. It is recommended to use `conda-forge` channel, but there are +others also. Other recommendation is that only one channel is used, to get +consistently build environments. + +Channels should be in priority order, where first one has highest priority. + +Example above uses `conda-forge` as its only channel. +For more details about conda-forge, see this [link.](https://anaconda.org/conda-forge) + +### What are `dependencies:`? + +These are libraries that are needed to be installed in environment that is +declared in this `conda.yaml` file. By default they come from locations +setup in `channels:` part of file. + +But there is also `- pip:` part and those dependenies come from +[PyPI](https://pypi.org/) and they are installed after dependencies from +`channels:` have been installed. + +In above example, `python=3.7.5` comes from `conda-forge` channel. +And `rpaframework==13.0.0` comes from [PyPI](https://pypi.org/project/rpaframework/). + +### What are `rccPostInstall:` scripts? + +Once environment dependencies have been installed, but before it is frozen as +hololib catalog, there is option to run some additional commands to customize +that environment. It is list of "shell" commands that are executed in order, +and if any of those fail, environment creation will fail. + +All those scripts must come from package declared in `dependencies:` section, +and should not use any "local" knowledge outside of environment under +construction. This makes environment creation repeatable and cacheable. + +Do not use any private or sensitive information in those post install scripts, +since result of environment build could be cached and visible to everybody +who has access to that cache. If you need to have private or sensitive packages +in your environment, see `preRunScripts` in `robot.yaml` file. + + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html @@ -371,6 +607,7 @@ That is rcc download site with two categories of: - tested versions (these are ones we ship with our tools) - latest 20 versions (which are not battle tested yet, but are bleeding edge) + ## What has changed on rcc? ### See changelog from git repo ... @@ -383,6 +620,7 @@ https://github.com/robocorp/rcc/blob/master/docs/changelog.md rcc docs changelog ``` + ## Can I see these tips as web page? Sure. See following URL. From e83af3b71907697b3f163bbc18a03106bdf6b2aa Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 6 Apr 2022 12:22:13 +0300 Subject: [PATCH 237/516] Documentation updates (v11.9.12) - added new `rcc man profiles` documentation command - more documentation updates - `Robocorp Cloud` to `Robocorp Control Room` related documentation changes --- Rakefile | 2 +- cmd/assistant.go | 4 ++-- cmd/cloud.go | 6 +++--- cmd/cloudNew.go | 4 ++-- cmd/configure.go | 2 +- cmd/credentials.go | 10 +++++----- cmd/download.go | 4 ++-- cmd/internale2ee.go | 2 +- cmd/issue.go | 4 ++-- cmd/metric.go | 4 ++-- cmd/profiles.go | 25 +++++++++++++++++++++++++ cmd/pull.go | 4 ++-- cmd/push.go | 4 ++-- cmd/robot.go | 4 ++-- cmd/root.go | 2 +- cmd/task.go | 4 ++-- cmd/upload.go | 4 ++-- cmd/userinfo.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ docs/environment-caching.md | 6 +++--- docs/features.md | 5 +++++ docs/robocorp_stack.png | Bin 17460 -> 101938 bytes docs/usecases.md | 1 + templates/extended/README.md | 4 ++-- 25 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 cmd/profiles.go diff --git a/Rakefile b/Rakefile index 17288c00..075fd092 100644 --- a/Rakefile +++ b/Rakefile @@ -30,7 +30,7 @@ task :assets do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md docs/features.md docs/usecases.md docs/recipes.md" + sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/*.md" end task :clean do diff --git a/cmd/assistant.go b/cmd/assistant.go index 1b80ef9f..0d8aa63c 100644 --- a/cmd/assistant.go +++ b/cmd/assistant.go @@ -9,11 +9,11 @@ var assistantCmd = &cobra.Command{ Aliases: []string{"assist", "a"}, Short: "Group of commands related to `robot assistant`.", Long: `This set of commands relate to Robocorp Robot Assistant related tasks. -They are either local, or in relation to Robocorp Cloud and Robocorp App.`, +They are either local, or in relation to Robocorp Control Room and Robocorp App.`, } func init() { rootCmd.AddCommand(assistantCmd) - assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", "Account used for Robocorp Cloud operations.") + assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", "Account used for Robocorp Control Room operations.") } diff --git a/cmd/cloud.go b/cmd/cloud.go index 2eda781f..cd98cdcd 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -7,12 +7,12 @@ import ( var cloudCmd = &cobra.Command{ Use: "cloud", Aliases: []string{"robocorp", "c"}, - Short: "Group of commands related to `Robocorp Cloud`.", - Long: `This group of commands apply to communication with Robocorp Cloud.`, + Short: "Group of commands related to `Robocorp Control Room`.", + Long: `This group of commands apply to communication with Robocorp Control Room.`, } func init() { rootCmd.AddCommand(cloudCmd) - cloudCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Cloud operations.") + cloudCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room operations.") } diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index 6adbb5f8..dd806a09 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -11,8 +11,8 @@ import ( var newCloudCmd = &cobra.Command{ Use: "new", - Short: "Create a new robot into Robocorp Cloud.", - Long: "Create a new robot into Robocorp Cloud.", + Short: "Create a new robot into Robocorp Control Room.", + Long: "Create a new robot into Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("New robot creation lasted").Report() diff --git a/cmd/configure.go b/cmd/configure.go index ba1bb655..fe0515db 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -14,5 +14,5 @@ var configureCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configureCmd) - configureCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Cloud task.") + configureCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room task.") } diff --git a/cmd/credentials.go b/cmd/credentials.go index 66f1d05a..bd4953a0 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -18,8 +18,8 @@ var ( var credentialsCmd = &cobra.Command{ Use: "credentials [credentials]", - Short: "Manage Robocorp Cloud API credentials.", - Long: "Manage Robocorp Cloud API credentials for later use.", + Short: "Manage Robocorp Control Room API credentials.", + Long: "Manage Robocorp Control Room API credentials for later use.", Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { @@ -54,7 +54,7 @@ var credentialsCmd = &cobra.Command{ } parts := strings.Split(credentials, ":") if len(parts) != 2 { - pretty.Exit(1, "Error: No valid credentials detected. Copy them from Robocorp Cloud.") + pretty.Exit(1, "Error: No valid credentials detected. Copy them from Robocorp Control Room.") } common.Log("Adding credentials: %v", parts) operations.UpdateCredentials(account, https, parts[0], parts[1]) @@ -80,9 +80,9 @@ func localDelete(accountName string) { func init() { configureCmd.AddCommand(credentialsCmd) - credentialsCmd.Flags().BoolVarP(&deleteCredentialsFlag, "delete", "", false, "Delete this account and corresponding Cloud credentials! DANGER!") + credentialsCmd.Flags().BoolVarP(&deleteCredentialsFlag, "delete", "", false, "Delete this account and corresponding Control Room credentials! DANGER!") credentialsCmd.Flags().BoolVarP(&defaultFlag, "default", "d", false, "Set this as the default account.") credentialsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") credentialsCmd.Flags().BoolVarP(&verifiedFlag, "verified", "v", false, "Updates the verified timestamp, if the credentials are still active.") - credentialsCmd.Flags().StringVarP(&endpointUrl, "endpoint", "e", "", "Robocorp Cloud endpoint used with the given account (or default).") + credentialsCmd.Flags().StringVarP(&endpointUrl, "endpoint", "e", "", "Robocorp Control Room endpoint used with the given account (or default).") } diff --git a/cmd/download.go b/cmd/download.go index a2199c68..b033bb82 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -11,8 +11,8 @@ import ( var downloadCmd = &cobra.Command{ Use: "download", - Short: "Fetch an existing robot from Robocorp Cloud.", - Long: "Fetch an existing robot from Robocorp Cloud.", + Short: "Fetch an existing robot from Robocorp Control Room.", + Long: "Fetch an existing robot from Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Download lasted").Report() diff --git a/cmd/internale2ee.go b/cmd/internale2ee.go index 6d4226c7..cf8ab28d 100644 --- a/cmd/internale2ee.go +++ b/cmd/internale2ee.go @@ -87,7 +87,7 @@ func version2encryption(args []string) { func init() { internalCmd.AddCommand(e2eeCmd) - e2eeCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Cloud operations.") + e2eeCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room operations.") e2eeCmd.Flags().IntVarP(&encryptionVersion, "use", "u", 1, "Which version of encryption method to test (1 or 2)") e2eeCmd.Flags().StringVarP(&workspaceId, "workspace", "", "", "Workspace id to get assistant information.") e2eeCmd.Flags().StringVarP(&assistantId, "assistant", "", "", "Assistant id to execute.") diff --git a/cmd/issue.go b/cmd/issue.go index ef377c7f..fce57df1 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -16,8 +16,8 @@ var ( var issueCmd = &cobra.Command{ Use: "issue", - Short: "Send an issue to Robocorp Cloud via rcc.", - Long: "Send an issue to Robocorp Cloud via rcc.", + Short: "Send an issue to Robocorp Control Room via rcc.", + Long: "Send an issue to Robocorp Control Room via rcc.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Feedback issue lasted").Report() diff --git a/cmd/metric.go b/cmd/metric.go index 475a7098..af2197e5 100644 --- a/cmd/metric.go +++ b/cmd/metric.go @@ -17,8 +17,8 @@ var ( var metricCmd = &cobra.Command{ Use: "metric", - Short: "Send some metric to Robocorp Cloud.", - Long: "Send some metric to Robocorp Cloud.", + Short: "Send some metric to Robocorp Control Room.", + Long: "Send some metric to Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Feedback metric lasted").Report() diff --git a/cmd/profiles.go b/cmd/profiles.go new file mode 100644 index 00000000..2c5ec206 --- /dev/null +++ b/cmd/profiles.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var manProfilesCmd = &cobra.Command{ + Use: "profiles", + Short: "Show configuration profiles documentation.", + Long: "Show configuration profiles documentation.", + Run: func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset("docs/profile_configuration.md") + if err != nil { + pretty.Exit(1, "Cannot show profile_configuration.md, reason: %v", err) + } + pretty.Page(content) + }, +} + +func init() { + manCmd.AddCommand(manProfilesCmd) +} diff --git a/cmd/pull.go b/cmd/pull.go index 78fafd36..c3eb7573 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -15,8 +15,8 @@ import ( var pullCmd = &cobra.Command{ Use: "pull", - Short: "Pull a robot from Robocorp Cloud and unwrap it into local directory.", - Long: "Pull a robot from Robocorp Cloud and unwrap it into local directory.", + Short: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", + Long: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Pull lasted").Report() diff --git a/cmd/push.go b/cmd/push.go index 6364f7e6..2544aa32 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -15,8 +15,8 @@ import ( var pushCmd = &cobra.Command{ Use: "push", - Short: "Wrap the local directory and push it into Robocorp Cloud as a specific robot.", - Long: "Wrap the local directory and push it into Robocorp Cloud as a specific robot.", + Short: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", + Long: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Push lasted").Report() diff --git a/cmd/robot.go b/cmd/robot.go index b6416187..cb484f66 100644 --- a/cmd/robot.go +++ b/cmd/robot.go @@ -8,8 +8,8 @@ var robotCmd = &cobra.Command{ Use: "robot", Aliases: []string{"r"}, Short: "Group of commands related to `robot`.", - Long: `This set of commands relate to Robocorp Cloud related tasks. They are -executed either locally, or in connection to Robocorp Cloud and Robocorp App.`, + Long: `This set of commands relate to Robocorp Control Room related tasks. They are +executed either locally, or in connection to Robocorp Control Room and Robocorp App.`, } func init() { diff --git a/cmd/root.go b/cmd/root.go index f39d6d79..e6378e83 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,7 +56,7 @@ var rootCmd = &cobra.Command{ Use: "rcc", Short: "rcc is environment manager for Robocorp Automation Stack", Long: `rcc provides support for creating and managing tasks, -communicating with Robocorp Cloud, and managing virtual environments where +communicating with Robocorp Control Room, and managing virtual environments where tasks can be developed, debugged, and run.`, Run: func(cmd *cobra.Command, args []string) { commandTree(0, "", cmd.Root()) diff --git a/cmd/task.go b/cmd/task.go index bbc6ef83..27ba4bb2 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -8,8 +8,8 @@ var taskCmd = &cobra.Command{ Use: "task", Aliases: []string{"t"}, Short: "Group of commands related to `task`.", - Long: `This set of commands relate to Robocorp Cloud related tasks. They are -executed either locally, or in connection to Robocorp Cloud and Robocorp App.`, + Long: `This set of commands relate to Robocorp Control Room related tasks. They are +executed either locally, or in connection to Robocorp Control Room and Robocorp App.`, } func init() { diff --git a/cmd/upload.go b/cmd/upload.go index 23609490..8fb8e5f3 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -11,8 +11,8 @@ import ( var uploadCmd = &cobra.Command{ Use: "upload", - Short: "Push an existing robot to Robocorp Cloud.", - Long: "Push an existing robot to Robocorp Cloud.", + Short: "Push an existing robot to Robocorp Control Room.", + Long: "Push an existing robot to Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Upload lasted").Report() diff --git a/cmd/userinfo.go b/cmd/userinfo.go index 37238aee..570c29a9 100644 --- a/cmd/userinfo.go +++ b/cmd/userinfo.go @@ -14,8 +14,8 @@ import ( var userinfoCmd = &cobra.Command{ Use: "userinfo", Aliases: []string{"user"}, - Short: "Query user information from Robocorp Cloud.", - Long: "Query user information from Robocorp Cloud.", + Short: "Query user information from Robocorp Control Room.", + Long: "Query user information from Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Userinfo query lasted").Report() diff --git a/common/version.go b/common/version.go index e3451198..0c7abec0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.11` + Version = `v11.9.12` ) diff --git a/docs/changelog.md b/docs/changelog.md index c8387749..19b3d322 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.9.12 (date: 6.4.2022) + +- added new `rcc man profiles` documentation command +- more documentation updates +- `Robocorp Cloud` to `Robocorp Control Room` related documentation changes + ## v11.9.11 (date: 5.4.2022) - documentation updates diff --git a/docs/environment-caching.md b/docs/environment-caching.md index d1040681..4ce380fb 100644 --- a/docs/environment-caching.md +++ b/docs/environment-caching.md @@ -18,7 +18,7 @@ The [hotel analogy below](/docs/environment-caching.md#a-better-analogy-accommod The solution that goes by the working title `Holotree` is a big step up when it comes to efficiency, and it opens up a few significant doors for the future. The name for `Holotree` comes from the analogy to the [Holodeck in Star Trek](https://en.wikipedia.org/wiki/Holodeck). When things changed in the Holodeck, the "reload" only meant that, for example, a chair transformed into a table while the surroundings did not change. -Holotree is an environment cache that stores each unique file only once. When restoring an execution environment, it moves only the files that need to be moved or removed, reducing the disk usage and file I/O (input/output) actions. Fewer files to move and handle means fewer actions for the virus scanner in the local development setup, and in Robocorp Cloud, reduced CPU time. +Holotree is an environment cache that stores each unique file only once. When restoring an execution environment, it moves only the files that need to be moved or removed, reducing the disk usage and file I/O (input/output) actions. Fewer files to move and handle means fewer actions for the virus scanner in the local development setup, and in Robocorp Control Room, reduced CPU time. Holotree can fit hundreds of unique environments in a small space. @@ -28,7 +28,7 @@ Holotree can fit hundreds of unique environments in a small space. There are other significant changes and improvements in Holotree regarding [relocation]() and [file locking](https://en.wikipedia.org/wiki/File_locking). -The implementation is already available in [RCC v10](https://github.com/robocorp/rcc/blob/master/docs/changelog.md), and we are rolling out it to our developer tools, applications and Cloud during Q3 2021. +The implementation is already available in [RCC v10](https://github.com/robocorp/rcc/blob/master/docs/changelog.md), and we are rolling out it to our developer tools, applications and Control Room during Q3 2021. ### Relocation and file locking @@ -38,7 +38,7 @@ To move or [relocate]() th [File locking](https://en.wikipedia.org/wiki/File_locking) affects mainly Windows but can affect macOS and Linux, too. Trying to change a file simultaneously in more than one process is usually harmful and requires individual files for different executors for the execution environments. -For example, when using Robocorp Lab and VS Code extensions on the same machine, it would be nice if the environment files do not get locked. +For example, when using Robocorp VS Code extensions on the same machine, it would be nice if the environment files do not get locked. For this reason, RCC with Holotree provides a `space` -context so that each execution or client can choose a space where the execution happens. Furthermore, with the partial relocation support described above, the client's space is only made for and used by the clients' discretion, avoiding file collisions and giving control over disk space usage and file I/O to the client applications. diff --git a/docs/features.md b/docs/features.md index 03bf5c45..e8e297cf 100644 --- a/docs/features.md +++ b/docs/features.md @@ -11,3 +11,8 @@ * provides commands to communicate with Robocorp Control Room from command line * enable caching dormant environments in efficiently and activating them locally when required without need to reinstall anything +* diagnose robots and network settings to see if there is something that prevents + using robots in specific environment +* support multiple configuration profiles for different network locations and + conditions (remote, office, restricted networks, ...) +* running assistants from command line diff --git a/docs/robocorp_stack.png b/docs/robocorp_stack.png index 41299e84b6ea5d032cc7d42144127eac0402a9c7..8fbeeab63f71e710de2073e5fdac0eeb8074146e 100644 GIT binary patch literal 101938 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 z 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 F{{qrh*OUMN literal 17460 zcmajG2UwJ`(kPsFcj>)%S≫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 diff --git a/docs/usecases.md b/docs/usecases.md index 89bec9f2..a8156341 100644 --- a/docs/usecases.md +++ b/docs/usecases.md @@ -12,6 +12,7 @@ for ideas what is available) * provide above things in computers, where internet access is restricted or prohibited (using pre-made hololib.zip environments) +* pull and run community created robots without Control Room requirement ## What is available from conda-forge? diff --git a/templates/extended/README.md b/templates/extended/README.md index 0070b9dd..4a7d5552 100644 --- a/templates/extended/README.md +++ b/templates/extended/README.md @@ -11,12 +11,12 @@ Run the robot locally: rcc run ``` -Provide access credentials for Robocorp Cloud connectivity: +Provide access credentials for Robocorp Control Room connectivity: ``` rcc configure credentials ``` -Upload to Robocorp Cloud: +Upload to Robocorp Control Room: ``` rcc cloud push --workspace --robot ``` From 446159fbbf96bb0a8dd2be42f451061ff9964f51 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 6 Apr 2022 14:47:16 +0300 Subject: [PATCH 238/516] Documentation table of contents (v11.9.13) - added new script for generating table of contents for docs - generated first table of contents as `docs/README.md` file --- common/version.go | 2 +- docs/README.md | 70 +++++++++++++++++++++++++++++++++++++++++++ docs/changelog.md | 5 ++++ scripts/toc.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 docs/README.md create mode 100755 scripts/toc.py diff --git a/common/version.go b/common/version.go index 0c7abec0..b53604ed 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.12` + Version = `v11.9.13` ) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..6b36133f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ +# [ToC for rcc documentation](https://github.com/robocorp/rcc/blob/master/) +## [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) +### [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) +## [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) +## [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies) +### [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes) +#### [Why is this important?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-is-this-important) +#### [Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-of-dependencies-listing-from-holotree-environment) +### [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies) +#### [Steps](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#steps) +#### [Limitations](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#limitations) +### [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli) +#### [Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-robot-yaml-with-scripting-task) +#### [Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#run-it-with-separator) +### [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment) +#### [Some example commands](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#some-example-commands) +### [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc) +#### [Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#basic-workflow-to-get-it-up-and-running) +#### [What next?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-next) +### [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework) +#### [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do) +#### [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robot-yaml) +#### [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-conda-yaml) +#### [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-builder-sh) +### [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) +#### [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) +#### [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) +### [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robot-yaml) +#### [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robot-yaml-thing) +#### [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-conda-yaml) +#### [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-conda-yaml-thing) +#### [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo) +#### [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly) +### [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +## [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) +### [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) +#### [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) +#### [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) +### [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) +### [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) +### [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) +### [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) +### [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc) +#### [Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#relocation-and-file-locking) +### [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations) +#### ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) +#### ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) +#### ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) +## [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc-how-to-build-it) +### [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) +### [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) +### [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 19b3d322..3d0f5806 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.9.13 (date: 6.4.2022) + +- added new script for generating table of contents for docs +- generated first table of contents as `docs/README.md` file + ## v11.9.12 (date: 6.4.2022) - added new `rcc man profiles` documentation command diff --git a/scripts/toc.py b/scripts/toc.py new file mode 100755 index 00000000..b14d8272 --- /dev/null +++ b/scripts/toc.py @@ -0,0 +1,76 @@ +#!/bin/env python3 + +import glob +import re + +NONCHAR_PATTERN = re.compile(r'[^a-z]+') +HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') +CODE_PATTERN = re.compile(r'^\s*[`]{3}') + +DASH = '-' +NEWLINE = '\n' + +IGNORE_LIST = ( + 'docs/changelog.md', + 'docs/toc.md', + 'docs/README.md', + ) + +PRIORITY_LIST = ( + 'docs/usecases.md', + 'docs/features.md', + 'docs/recipes.md', + 'docs/profile_configuration.md', + 'docs/environment-caching.md', + ) + +def unify(value): + return DASH.join(filter(bool, NONCHAR_PATTERN.split(str(value).lower()))) + +class Toc: + def __init__(self, title, baseurl): + self.title = title + self.baseurl = baseurl + self.levels = [0,0,0,0] + self.level = 0 + self.toc = [f'# [{title}]({baseurl})'] + + def add(self, filename, level, title): + url = f'{self.baseurl}{filename}' + prefix = '#' * level + ref = unify(title) + self.toc.append(f'#{prefix} [{title}]({self.baseurl}{filename}#{ref})') + + def write(self, filename): + with open(filename, 'w+') as sink: + sink.write(NEWLINE.join(self.toc)) + +def headings(filename): + inside = False + with open(filename) as source: + for line in source: + if CODE_PATTERN.match(line): + inside = not inside + if inside: + continue + if found := HEADING_PATTERN.match(line): + level, title = found.groups() + yield filename, len(level), title + +def process(): + toc = Toc("ToC for rcc documentation", "https://github.com/robocorp/rcc/blob/master/") + documentation = list(glob.glob('docs/*.md')) + for filename in PRIORITY_LIST: + if filename in documentation: + documentation.remove(filename) + for filename, level, title in headings(filename): + toc.add(filename, level, title) + for filename in documentation: + if filename in IGNORE_LIST: + continue + for filename, level, title in headings(filename): + toc.add(filename, level, title) + toc.write('docs/README.md') + +if __name__ == '__main__': + process() From e4a61c60852ece35d1e8f068b9ccc0ee5d715b2b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 6 Apr 2022 15:12:58 +0300 Subject: [PATCH 239/516] Documentation table of contents (v11.9.14) - improved table of contents with numbering --- common/version.go | 2 +- docs/README.md | 140 +++++++++++++++++++++++----------------------- docs/changelog.md | 4 ++ scripts/toc.py | 23 ++++++-- 4 files changed, 93 insertions(+), 76 deletions(-) diff --git a/common/version.go b/common/version.go index b53604ed..6e9fc493 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.13` + Version = `v11.9.14` ) diff --git a/docs/README.md b/docs/README.md index 6b36133f..a284072c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,70 +1,70 @@ -# [ToC for rcc documentation](https://github.com/robocorp/rcc/blob/master/) -## [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) -### [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) -## [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) -## [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies) -### [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes) -#### [Why is this important?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-is-this-important) -#### [Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-of-dependencies-listing-from-holotree-environment) -### [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies) -#### [Steps](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#steps) -#### [Limitations](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#limitations) -### [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli) -#### [Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-robot-yaml-with-scripting-task) -#### [Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#run-it-with-separator) -### [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment) -#### [Some example commands](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#some-example-commands) -### [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc) -#### [Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#basic-workflow-to-get-it-up-and-running) -#### [What next?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-next) -### [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework) -#### [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do) -#### [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robot-yaml) -#### [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-conda-yaml) -#### [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-builder-sh) -### [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) -#### [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) -#### [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) -### [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robot-yaml) -#### [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robot-yaml-thing) -#### [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-conda-yaml) -#### [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-conda-yaml-thing) -#### [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo) -#### [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly) -### [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) -## [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) -### [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) -#### [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) -#### [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) -### [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) -### [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) -### [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) -### [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) -### [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc) -#### [Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#relocation-and-file-locking) -### [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations) -#### ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) -#### ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) -#### ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc-how-to-build-it) -### [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) -### [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) -### [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file +# ToC for rcc documentation +## [1 Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) +### [1.1 What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) +## [2 Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) +## [3 Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies) +### [3.1 How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes) +#### [3.1.1 Why is this important?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-is-this-important) +#### [3.1.2 Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-of-dependencies-listing-from-holotree-environment) +### [3.2 How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies) +#### [3.2.1 Steps](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#steps) +#### [3.2.2 Limitations](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#limitations) +### [3.3 How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli) +#### [3.3.1 Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-robotyaml-with-scripting-task) +#### [3.3.2 Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#run-it-with-separator) +### [3.4 How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment) +#### [3.4.1 Some example commands](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#some-example-commands) +### [3.5 How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc) +#### [3.5.1 Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#basic-workflow-to-get-it-up-and-running) +#### [3.5.2 What next?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-next) +### [3.6 Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework) +#### [3.6.1 This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do) +#### [3.6.2 Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robotyaml) +#### [3.6.3 Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-condayaml) +#### [3.6.4 Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-buildersh) +### [3.7 How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) +#### [3.7.1 How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) +#### [3.7.2 How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) +### [3.8 What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### [3.9 How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### [3.9.1 Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### [3.10 What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) +#### [3.10.1 Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### [3.10.2 What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### [3.10.3 What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### [3.10.4 What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### [3.10.5 What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### [3.10.6 What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### [3.10.7 What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### [3.10.8 What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### [3.10.9 What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### [3.10.10 What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### [3.11 What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### [3.11.1 Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### [3.11.2 What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### [3.11.3 What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### [3.11.4 What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### [3.11.5 What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### [3.12 Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### [3.13 What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### [3.13.1 See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo) +#### [3.13.2 See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly) +### [3.14 Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +## [4 Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) +### [4.1 What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) +#### [4.1.1 When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) +#### [4.1.2 What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) +### [4.2 Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) +### [4.3 What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) +### [4.4 Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) +### [4.5 What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) +### [4.6 The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc) +#### [4.6.1 Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#relocation-and-file-locking) +### [4.7 A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations) +#### [4.7.1 "I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) +#### [4.7.2 "Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) +#### [4.7.3 "Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) +## [5 rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc-how-to-build-it) +### [5.1 Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) +### [5.2 Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) +### [5.3 Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 3d0f5806..b76d4080 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.9.14 (date: 6.4.2022) + +- improved table of contents with numbering + ## v11.9.13 (date: 6.4.2022) - added new script for generating table of contents for docs diff --git a/scripts/toc.py b/scripts/toc.py index b14d8272..c105b07b 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -7,6 +7,7 @@ HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') CODE_PATTERN = re.compile(r'^\s*[`]{3}') +DOT = '.' DASH = '-' NEWLINE = '\n' @@ -25,21 +26,33 @@ ) def unify(value): - return DASH.join(filter(bool, NONCHAR_PATTERN.split(str(value).lower()))) + low = str(value).lower().replace('.', '') + return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))) class Toc: def __init__(self, title, baseurl): self.title = title self.baseurl = baseurl - self.levels = [0,0,0,0] - self.level = 0 - self.toc = [f'# [{title}]({baseurl})'] + self.levels = [0] + self.toc = [f'# {title}'] + + def leveling(self, level): + levelup = True + while len(self.levels) > level: + self.levels.pop() + while len(self.levels) < level: + self.levels.append(1) + levelup = False + if levelup: + self.levels[-1] += 1 def add(self, filename, level, title): + self.leveling(level) + numbering = DOT.join(map(str, self.levels)) url = f'{self.baseurl}{filename}' prefix = '#' * level ref = unify(title) - self.toc.append(f'#{prefix} [{title}]({self.baseurl}{filename}#{ref})') + self.toc.append(f'#{prefix} [{numbering} {title}]({self.baseurl}{filename}#{ref})') def write(self, filename): with open(filename, 'w+') as sink: From 530afa8ecfb14a0f372914832d4a3649b22271cf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 6 Apr 2022 16:24:41 +0300 Subject: [PATCH 240/516] Documentation table of contents (v11.9.15) - improved table of contents with more clearer numbering --- common/version.go | 2 +- docs/README.md | 138 +++++++++++++++++++++++----------------------- docs/changelog.md | 4 ++ scripts/toc.py | 8 +-- 4 files changed, 78 insertions(+), 74 deletions(-) diff --git a/common/version.go b/common/version.go index 6e9fc493..00b8223b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.14` + Version = `v11.9.15` ) diff --git a/docs/README.md b/docs/README.md index a284072c..cb07a260 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,70 +1,70 @@ # ToC for rcc documentation -## [1 Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) -### [1.1 What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) -## [2 Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) -## [3 Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies) -### [3.1 How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes) -#### [3.1.1 Why is this important?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-is-this-important) -#### [3.1.2 Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-of-dependencies-listing-from-holotree-environment) -### [3.2 How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies) -#### [3.2.1 Steps](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#steps) -#### [3.2.2 Limitations](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#limitations) -### [3.3 How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli) -#### [3.3.1 Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-robotyaml-with-scripting-task) -#### [3.3.2 Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#run-it-with-separator) -### [3.4 How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment) -#### [3.4.1 Some example commands](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#some-example-commands) -### [3.5 How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc) -#### [3.5.1 Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#basic-workflow-to-get-it-up-and-running) -#### [3.5.2 What next?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-next) -### [3.6 Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework) -#### [3.6.1 This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do) -#### [3.6.2 Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robotyaml) -#### [3.6.3 Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-condayaml) -#### [3.6.4 Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-buildersh) -### [3.7 How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) -#### [3.7.1 How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) -#### [3.7.2 How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### [3.8 What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) -### [3.9 How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### [3.9.1 Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### [3.10 What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) -#### [3.10.1 Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### [3.10.2 What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### [3.10.3 What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### [3.10.4 What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### [3.10.5 What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### [3.10.6 What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### [3.10.7 What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### [3.10.8 What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### [3.10.9 What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### [3.10.10 What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### [3.11 What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) -#### [3.11.1 Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### [3.11.2 What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) -#### [3.11.3 What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### [3.11.4 What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### [3.11.5 What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### [3.12 Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### [3.13 What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### [3.13.1 See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo) -#### [3.13.2 See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly) -### [3.14 Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) -## [4 Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) -### [4.1 What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) -#### [4.1.1 When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) -#### [4.1.2 What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) -### [4.2 Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) -### [4.3 What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) -### [4.4 Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) -### [4.5 What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) -### [4.6 The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc) -#### [4.6.1 Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#relocation-and-file-locking) -### [4.7 A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations) -#### [4.7.1 "I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) -#### [4.7.2 "Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) -#### [4.7.3 "Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## [5 rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc-how-to-build-it) -### [5.1 Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) -### [5.2 Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) -### [5.3 Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file +## 1 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) +### 1.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) +## 2 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) +## 3 [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies) +### 3.1 [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes) +#### 3.1.1 [Why is this important?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-is-this-important) +#### 3.1.2 [Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-of-dependencies-listing-from-holotree-environment) +### 3.2 [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies) +#### 3.2.1 [Steps](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#steps) +#### 3.2.2 [Limitations](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#limitations) +### 3.3 [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli) +#### 3.3.1 [Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example-robotyaml-with-scripting-task) +#### 3.3.2 [Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#run-it-with----separator) +### 3.4 [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment) +#### 3.4.1 [Some example commands](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#some-example-commands) +### 3.5 [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc) +#### 3.5.1 [Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#basic-workflow-to-get-it-up-and-running) +#### 3.5.2 [What next?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-next) +### 3.6 [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework) +#### 3.6.1 [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do-) +#### 3.6.2 [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robotyaml) +#### 3.6.3 [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-condayaml) +#### 3.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-buildersh) +### 3.7 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) +#### 3.7.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) +#### 3.7.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) +### 3.8 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### 3.9 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### 3.9.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### 3.10 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) +#### 3.10.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.10.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### 3.10.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.10.4 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.10.5 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.10.6 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.10.7 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.10.8 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.10.9 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.10.10 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### 3.11 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### 3.11.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.11.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### 3.11.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### 3.11.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### 3.11.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### 3.12 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.13 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.13.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.13.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.14 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) +### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) +#### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) +#### 4.1.2 [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) +### 4.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) +### 4.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) +### 4.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) +### 4.5 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) +### 4.6 [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc) +#### 4.6.1 [Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#relocation-and-file-locking) +### 4.7 [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations) +#### 4.7.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) +#### 4.7.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) +#### 4.7.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) +## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it) +### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) +### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) +### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index b76d4080..a63cf66e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.9.15 (date: 6.4.2022) + +- improved table of contents with more clearer numbering + ## v11.9.14 (date: 6.4.2022) - improved table of contents with numbering diff --git a/scripts/toc.py b/scripts/toc.py index c105b07b..1e69bc3d 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -3,7 +3,7 @@ import glob import re -NONCHAR_PATTERN = re.compile(r'[^a-z]+') +NONCHAR_PATTERN = re.compile(r'[^.a-z-]+') HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') CODE_PATTERN = re.compile(r'^\s*[`]{3}') @@ -26,8 +26,8 @@ ) def unify(value): - low = str(value).lower().replace('.', '') - return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))) + low = str(value).lower() + return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))).replace('.', '') class Toc: def __init__(self, title, baseurl): @@ -52,7 +52,7 @@ def add(self, filename, level, title): url = f'{self.baseurl}{filename}' prefix = '#' * level ref = unify(title) - self.toc.append(f'#{prefix} [{numbering} {title}]({self.baseurl}{filename}#{ref})') + self.toc.append(f'#{prefix} {numbering} [{title}]({self.baseurl}{filename}#{ref})') def write(self, filename): with open(filename, 'w+') as sink: From ed280dfe78d42e952271a26d999f6fa66e3767f3 Mon Sep 17 00:00:00 2001 From: Cosmin Poieana Date: Wed, 6 Apr 2022 19:09:23 +0200 Subject: [PATCH 241/516] Fix typos in recipes doc --- docs/recipes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.md b/docs/recipes.md index 284b56f7..4680a07a 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -288,7 +288,7 @@ is going to be only one real environment available. But above three controls gives you good ways to control how you and your applications manage their usage of different python environments for different purposes. You can share environments if you want, but you can also -give dedicates space for thos things that need full control of their space. +give a dedicated space for those things that need full control of their space. So running following commands demonstrate different levels of control for space creation. From ef389604f74e1567e94ec048d0fa62774200be52 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 7 Apr 2022 12:14:54 +0300 Subject: [PATCH 242/516] Documentation pull request (v11.9.16) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 00b8223b..d6501fe7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.15` + Version = `v11.9.16` ) diff --git a/docs/changelog.md b/docs/changelog.md index a63cf66e..ae46662e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.9.16 (date: 7.4.2022) + +- took pull request with documentation fixes + ## v11.9.15 (date: 6.4.2022) - improved table of contents with more clearer numbering From e5a3995319ce769c699a12b8d7fc07d448c66dda Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 12 Apr 2022 10:22:56 +0300 Subject: [PATCH 243/516] Holotree relocation revisited (v11.10.0) - work in progess: holotree relocation revisiting started - now holotree library and catalog can be on shared location --- cmd/holotreeInit.go | 33 +++++++++++++++++++ common/platform_darwin.go | 1 + common/platform_linux.go | 1 + common/platform_windows.go | 1 + common/variables.go | 58 ++++++++++++++++++++++++++++----- common/version.go | 2 +- conda/activate.go | 1 + conda/platform_darwin_amd64.go | 4 +-- conda/platform_linux_amd64.go | 4 +-- conda/platform_windows_amd64.go | 4 +-- docs/changelog.md | 5 +++ htfs/directory.go | 4 +++ htfs/functions.go | 3 +- htfs/library.go | 2 +- pathlib/copyfile.go | 3 +- pathlib/functions.go | 55 +++++++++++++++++++++++++++++-- 16 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 cmd/holotreeInit.go diff --git a/cmd/holotreeInit.go b/cmd/holotreeInit.go new file mode 100644 index 00000000..bfb4cd93 --- /dev/null +++ b/cmd/holotreeInit.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "os" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var holotreeInitCmd = &cobra.Command{ + Use: "init", + Short: "Initialize shared holotree location.", + Long: "Initialize shared holotree location.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Conda YAML hash calculation lasted").Report() + } + pretty.Guard(common.FixedHolotreeLocation(), 1, "Fixed Holotree is not available in this system!") + if os.Geteuid() > 0 { + pretty.Warning("Running this command might need sudo/root access rights. Still, trying ...") + } + _, err := pathlib.MakeSharedDir(common.HoloLocation()) + pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.HoloLocation(), err) + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeInitCmd) +} diff --git a/common/platform_darwin.go b/common/platform_darwin.go index 1678ffa4..85661e26 100644 --- a/common/platform_darwin.go +++ b/common/platform_darwin.go @@ -7,6 +7,7 @@ import ( const ( defaultRobocorpLocation = "$HOME/.robocorp" + defaultHoloLocation = "/Users/Shared/robocorp/ht" ) func ExpandPath(entry string) string { diff --git a/common/platform_linux.go b/common/platform_linux.go index 1678ffa4..9e62374f 100644 --- a/common/platform_linux.go +++ b/common/platform_linux.go @@ -7,6 +7,7 @@ import ( const ( defaultRobocorpLocation = "$HOME/.robocorp" + defaultHoloLocation = "/opt/robocorp/ht" ) func ExpandPath(entry string) string { diff --git a/common/platform_windows.go b/common/platform_windows.go index 49bd24b2..c250746c 100644 --- a/common/platform_windows.go +++ b/common/platform_windows.go @@ -8,6 +8,7 @@ import ( const ( defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" + defaultHoloLocation = "c:\\ProgramData\\robocorp\\ht" ) var ( diff --git a/common/variables.go b/common/variables.go index 54b0cc64..5f2b40af 100644 --- a/common/variables.go +++ b/common/variables.go @@ -8,9 +8,12 @@ import ( "runtime" "strings" "time" + + "github.com/dchest/siphash" ) const ( + FIXED_HOLOTREE = `FIXED_HOLOTREE` ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` @@ -43,11 +46,14 @@ func init() { ProgressMark = time.Now() randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) + + // Note: HololibCatalogLocation and HololibLibraryLocation are force + // created from "htfs" direcotry.go init function + // Also: HolotreeLocation creation is left for actual holotree commands + // to prevent accidental access right problem during usage + ensureDirectory(TemplateLocation()) ensureDirectory(BinLocation()) - ensureDirectory(HolotreeLocation()) - ensureDirectory(HololibCatalogLocation()) - ensureDirectory(HololibLibraryLocation()) ensureDirectory(PipCache()) ensureDirectory(WheelCache()) ensureDirectory(RobotCache()) @@ -114,7 +120,25 @@ func BinLocation() string { return filepath.Join(RobocorpHome(), "bin") } +func HoloLocation() string { + return ExpandPath(defaultHoloLocation) +} + +func FixedHolotreeLocation() bool { + return len(os.Getenv(FIXED_HOLOTREE)) > 0 +} + +func HolotreeLocation() string { + if FixedHolotreeLocation() { + return filepath.Join(HoloLocation(), userHomeIdentity()) + } + return filepath.Join(RobocorpHome(), "holotree") +} + func HololibLocation() string { + if FixedHolotreeLocation() { + return HoloLocation() + } return filepath.Join(RobocorpHome(), "hololib") } @@ -130,10 +154,6 @@ func HolotreeLock() string { return fmt.Sprintf("%s.lck", HolotreeLocation()) } -func HolotreeLocation() string { - return filepath.Join(RobocorpHome(), "holotree") -} - func UsesHolotree() bool { return len(HolotreeSpace) > 0 } @@ -150,8 +170,12 @@ func RobotCache() string { return filepath.Join(RobocorpHome(), "robots") } +func MambaRootPrefix() string { + return RobocorpHome() +} + func MambaPackages() string { - return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) + return ExpandPath(filepath.Join(MambaRootPrefix(), "pkgs")) } func PipRcFile() string { @@ -204,6 +228,22 @@ func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } +func isDir(pathname string) bool { + stat, err := os.Stat(pathname) + return err == nil && stat.IsDir() +} + func ensureDirectory(name string) { - Error("mkdir", os.MkdirAll(name, 0o750)) + if !isDir(name) { + Error("mkdir", os.MkdirAll(name, 0o750)) + } +} + +func userHomeIdentity() string { + location, err := os.UserHomeDir() + if err != nil { + return "1badcafe" + } + digest := fmt.Sprintf("%02x", siphash.Hash(9007799254740993, 2147487647, []byte(location))) + return digest[:8] } diff --git a/common/version.go b/common/version.go index d6501fe7..6bcbb1ad 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.9.16` + Version = `v11.10.0` ) diff --git a/conda/activate.go b/conda/activate.go index 63ed582e..7e7466a2 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -55,6 +55,7 @@ func createScript(targetFolder string) (string, error) { details := make(map[string]string) details["Rcc"] = common.BinRcc() details["Robocorphome"] = common.RobocorpHome() + details["MambaRootPrefix"] = common.MambaRootPrefix() details["Micromamba"] = BinMicromamba() details["Live"] = targetFolder buffer := bytes.NewBuffer(nil) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 8bafada1..cbde0971 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -14,7 +14,7 @@ const ( binSuffix = "/bin" activateScript = `#!/bin/bash -export MAMBA_ROOT_PREFIX={{.Robocorphome}} +export MAMBA_ROOT_PREFIX={{.MambaRootPrefix}} eval "$('{{.Micromamba}}' shell activate -s bash -p {{.Live}})" "{{.Rcc}}" internal env -l after ` @@ -28,7 +28,7 @@ var ( func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 83d39e2d..5bd3d013 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -14,7 +14,7 @@ const ( binSuffix = "/bin" activateScript = `#!/bin/bash -export MAMBA_ROOT_PREFIX={{.Robocorphome}} +export MAMBA_ROOT_PREFIX={{.MambaRootPrefix}} eval "$('{{.Micromamba}}' shell activate -s bash -p {{.Live}})" "{{.Rcc}}" internal env -l after ` @@ -32,7 +32,7 @@ func MicromambaLink() string { func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 99931ddb..bd676f70 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -21,7 +21,7 @@ const ( usrSuffix = "\\usr" binSuffix = "\\bin" activateScript = "@echo off\n" + - "set \"MAMBA_ROOT_PREFIX={{.Robocorphome}}\"\n" + + "set \"MAMBA_ROOT_PREFIX={{.MambaRootPrefix}}\"\n" + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell -s cmd.exe activate -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + "call \"{{.Rcc}}\" internal env -l after\n" commandSuffix = ".cmd" @@ -38,7 +38,7 @@ var ( func CondaEnvironment() []string { env := os.Environ() - env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) + env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) diff --git a/docs/changelog.md b/docs/changelog.md index ae46662e..e0f64448 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.10.0 (date: 11.4.2022) UNSTABLE + +- work in progess: holotree relocation revisiting started +- now holotree library and catalog can be on shared location + ## v11.9.16 (date: 7.4.2022) - took pull request with documentation fixes diff --git a/htfs/directory.go b/htfs/directory.go index e254cc6b..32115e97 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -28,6 +28,10 @@ func init() { killfile[".hg"] = true killfile[".svn"] = true killfile[".gitignore"] = true + + pathlib.MakeSharedDir(common.HoloLocation()) + pathlib.MakeSharedDir(common.HololibCatalogLocation()) + pathlib.MakeSharedDir(common.HololibLibraryLocation()) } type Filetask func(string, *File) anywork.Work diff --git a/htfs/functions.go b/htfs/functions.go index 89525e3a..068eb679 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -174,7 +174,7 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { seen[file.Digest] = true directory := library.Location(file.Digest) if !seen[directory] && !pathlib.IsDir(directory) { - os.MkdirAll(directory, 0o755) + pathlib.MakeSharedDir(directory) } seen[directory] = true sinkpath := filepath.Join(directory, file.Digest) @@ -264,6 +264,7 @@ func LiftFile(sourcename, sinkname string) anywork.Work { runtime.Gosched() anywork.OnErrPanicCloseAll(TryRename("liftfile", partname, sinkname)) + pathlib.MakeSharedFile(sinkname) } } diff --git a/htfs/library.go b/htfs/library.go index 87429e7e..054219b7 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -356,7 +356,7 @@ func makedirs(prefix string, suffixes ...string) error { } for _, suffix := range suffixes { fullpath := filepath.Join(prefix, suffix) - err := os.MkdirAll(fullpath, 0o755) + _, err := pathlib.MakeSharedDir(fullpath) if err != nil { return err } diff --git a/pathlib/copyfile.go b/pathlib/copyfile.go index 993ede6c..5148c190 100644 --- a/pathlib/copyfile.go +++ b/pathlib/copyfile.go @@ -23,8 +23,7 @@ func CopyFile(source, target string, overwrite bool) error { } func copyFile(source, target string, overwrite bool, copier copyfunc) error { - targetDir := filepath.Dir(target) - err := os.MkdirAll(targetDir, 0o755) + _, err := MakeSharedDir(filepath.Dir(target)) if err != nil { return err } diff --git a/pathlib/functions.go b/pathlib/functions.go index aca11543..9ab2f2fa 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -2,9 +2,12 @@ package pathlib import ( "fmt" + "io/fs" "os" "path/filepath" "time" + + "github.com/robocorp/rcc/fail" ) func Exists(pathname string) bool { @@ -46,12 +49,56 @@ func Modtime(pathname string) (time.Time, error) { return stat.ModTime(), nil } -func EnsureDirectory(directory string) (string, error) { +func ensureCorrectMode(fullpath string, stat fs.FileInfo, correct fs.FileMode) (string, error) { + mode := stat.Mode() & correct + if mode == correct { + return fullpath, nil + } + err := os.Chmod(fullpath, correct) + if err != nil { + return "", err + } + return fullpath, nil +} + +func makeModedDir(fullpath string, correct fs.FileMode) (path string, err error) { + defer fail.Around(&err) + + stat, err := os.Stat(fullpath) + if err == nil && stat.IsDir() { + return ensureCorrectMode(fullpath, stat, correct) + } + fail.On(err == nil, "Path %q exists, but is not a directory!", fullpath) + _, err = MakeSharedDir(filepath.Dir(fullpath)) + fail.On(err != nil, "%v", err) + err = os.Mkdir(fullpath, correct) + fail.On(err != nil, "Failed to create directory %q, reason: %v", fullpath, err) + stat, err = os.Stat(fullpath) + fail.On(err != nil, "Failed to stat created directory %q, reason: %v", fullpath, err) + _, err = ensureCorrectMode(fullpath, stat, correct) + fail.On(err != nil, "Failed to make created directory shared %q, reason: %v", fullpath, err) + return fullpath, nil +} + +func MakeSharedFile(fullpath string) (string, error) { + stat, err := os.Stat(fullpath) + fail.On(err != nil, "Failed to stat file %q, reason: %v", fullpath, err) + return ensureCorrectMode(fullpath, stat, 0666) +} + +func MakeSharedDir(fullpath string) (string, error) { + return makeModedDir(fullpath, 0777) +} + +func doEnsureDirectory(directory string, mode fs.FileMode) (string, error) { fullpath, err := filepath.Abs(directory) if err != nil { return "", err } - err = os.MkdirAll(fullpath, 0o750) + if IsDir(fullpath) { + return fullpath, nil + } + err = os.MkdirAll(fullpath, mode) if err != nil { return "", err } @@ -62,6 +109,10 @@ func EnsureDirectory(directory string) (string, error) { return fullpath, nil } +func EnsureDirectory(directory string) (string, error) { + return doEnsureDirectory(directory, 0o750) +} + func EnsureParentDirectory(resource string) (string, error) { return EnsureDirectory(filepath.Dir(resource)) } From 7c51f295a3865a56548c52797e767d0d7642179f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 12 Apr 2022 14:34:18 +0300 Subject: [PATCH 244/516] Holotree relocation revisited (v11.10.1) --- cmd/diagnostics.go | 7 ++++--- common/variables.go | 8 ++++---- common/version.go | 2 +- docs/changelog.md | 8 +++++++- htfs/commands.go | 3 +++ htfs/library.go | 7 ++++--- htfs/virtual.go | 3 ++- operations/diagnostics.go | 6 ++++++ robot_tests/export_holozip.robot | 16 ++++++++-------- robot_tests/fullrun.robot | 4 ++-- robot_tests/templates.robot | 2 +- 11 files changed, 42 insertions(+), 24 deletions(-) diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 01e0bee6..66c151d3 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -14,9 +14,10 @@ var ( ) var diagnosticsCmd = &cobra.Command{ - Use: "diagnostics", - Short: "Run system diagnostics to help resolve rcc issues.", - Long: "Run system diagnostics to help resolve rcc issues.", + Use: "diagnostics", + Aliases: []string{"diagnostic", "diag"}, + Short: "Run system diagnostics to help resolve rcc issues.", + Long: "Run system diagnostics to help resolve rcc issues.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() diff --git a/common/variables.go b/common/variables.go index 5f2b40af..5827e9ba 100644 --- a/common/variables.go +++ b/common/variables.go @@ -130,7 +130,7 @@ func FixedHolotreeLocation() bool { func HolotreeLocation() string { if FixedHolotreeLocation() { - return filepath.Join(HoloLocation(), userHomeIdentity()) + return HoloLocation() } return filepath.Join(RobocorpHome(), "holotree") } @@ -239,11 +239,11 @@ func ensureDirectory(name string) { } } -func userHomeIdentity() string { +func UserHomeIdentity() string { location, err := os.UserHomeDir() if err != nil { - return "1badcafe" + return "badcafe" } digest := fmt.Sprintf("%02x", siphash.Hash(9007799254740993, 2147487647, []byte(location))) - return digest[:8] + return digest[:7] } diff --git a/common/version.go b/common/version.go index 6bcbb1ad..a51ff693 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.0` + Version = `v11.10.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index e0f64448..d3186be3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,12 @@ # rcc change log -## v11.10.0 (date: 11.4.2022) UNSTABLE +## v11.10.1 (date: 12.4.2022) UNSTABLE + +- adding support for user identity in relocation +- added holotree locations into diagnostics +- usage of hololib.zip now has debug entry in log files + +## v11.10.0 (date: 12.4.2022) UNSTABLE - work in progess: holotree relocation revisiting started - now holotree library and catalog can be on shared location diff --git a/htfs/commands.go b/htfs/commands.go index 132bf5b0..e344f11b 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -28,6 +28,9 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin defer locker.Release() haszip := len(holozip) > 0 + if haszip { + common.Debug("New zipped environment from %q!", holozip) + } _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) diff --git a/htfs/library.go b/htfs/library.go index 054219b7..094a7ca6 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -83,7 +83,8 @@ func (it *hololib) ExactLocation(digest string) string { } func (it *hololib) Identity() string { - return fmt.Sprintf("h%016xt", it.identity) + suffix := fmt.Sprintf("%016x", it.identity) + return fmt.Sprintf("h%s_%st", common.UserHomeIdentity(), suffix[:14]) } func (it *hololib) Stage() string { @@ -263,9 +264,9 @@ func Spaces() []*Root { } func ControllerSpaceName(client, tag []byte) string { - prefix := textual(sipit(client), 9) + prefix := textual(sipit(client), 7) suffix := textual(sipit(tag), 8) - return prefix + "_" + suffix + return common.UserHomeIdentity() + "_" + prefix + "_" + suffix } func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { diff --git a/htfs/virtual.go b/htfs/virtual.go index b9df7f36..6fdb0c3d 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -26,7 +26,8 @@ func Virtual() MutableLibrary { } func (it *virtual) Identity() string { - return fmt.Sprintf("v%016xh", it.identity) + suffix := fmt.Sprintf("%016x", it.identity) + return fmt.Sprintf("v%s_%sh", common.UserHomeIdentity(), suffix[:14]) } func (it *virtual) Stage() string { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 364c96c9..1eee072a 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -86,6 +86,12 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["config-http-proxy"] = settings.Global.HttpProxy() result.Details["config-ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) result.Details["config-ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) + result.Details["os-holo-location"] = common.HoloLocation() + result.Details["hololib-location"] = common.HololibLocation() + result.Details["hololib-catalog-location"] = common.HololibCatalogLocation() + result.Details["hololib-library-location"] = common.HololibLibraryLocation() + result.Details["holotree-location"] = common.HolotreeLocation() + result.Details["holotree-user-id"] = common.UserHomeIdentity() result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 6b70b386..45be7b5a 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -29,7 +29,7 @@ Goal: Create environment for standalone robot Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml Must Have RCC_ENVIRONMENT_HASH= Must Have RCC_INSTALLATION_ID= - Must Have 4e67cd8d4_fcb4b859 + Must Have 4e67cd8_fcb4b859 Use STDERR Must Have Downloading micromamba Must Have Progress: 01/13 @@ -38,7 +38,7 @@ Goal: Create environment for standalone robot Goal: Must have author space visible Step build/rcc ht ls Use STDERR - Must Have 4e67cd8d4_fcb4b859 + Must Have 4e67cd8_fcb4b859 Must Have rcc.citests Must Have author Must Have 55aacd3b136421fd @@ -69,10 +69,10 @@ Goal: See contents of that robot Must Have hololib.zip Goal: Can delete author space - Step build/rcc ht delete 4e67cd8d4_fcb4b859 + Step build/rcc ht delete 4e67cd8_fcb4b859 Step build/rcc ht ls Use STDERR - Wont Have 4e67cd8d4_fcb4b859 + Wont Have 4e67cd8_fcb4b859 Wont Have rcc.citests Wont Have author Wont Have 55aacd3b136421fd @@ -89,20 +89,20 @@ Goal: No spaces created under guest Set Environment Variable ROBOCORP_HOME tmp/guest Step build/rcc ht ls Use STDERR - Wont Have 4e67cd8d4_fcb4b859 + Wont Have 4e67cd8_fcb4b859 Wont Have rcc.citests Wont Have author Wont Have 55aacd3b136421fd - Wont Have 4e67cd8d4_559e19be + Wont Have 4e67cd8_559e19be Wont Have guest Goal: Space created under author for guest Set Environment Variable ROBOCORP_HOME tmp/developer Step build/rcc ht ls Use STDERR - Wont Have 4e67cd8d4_fcb4b859 + Wont Have 4e67cd8_fcb4b859 Wont Have author Must Have rcc.citests Must Have 55aacd3b136421fd - Must Have 4e67cd8d4_aacf1552 + Must Have 4e67cd8_aacf1552 Must Have guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 37c419bb..9467d672 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -129,11 +129,11 @@ Goal: Merge two different conda.yaml files with conflict fails Goal: Merge two different conda.yaml files without conflict passes Step build/rcc holotree vars --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent Must Have 5bea0c1d2419493e - Must Have 4e67cd8d4_9fcd2534 + Must Have 4e67cd8_9fcd2534 Goal: Can list environments as JSON Step build/rcc holotree list --controller citests --json - Must Have 4e67cd8d4_9fcd2534 + Must Have 4e67cd8_9fcd2534 Must Have 5bea0c1d2419493e Must Be Json Response diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index 78af7468..9cd40bb8 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -60,7 +60,7 @@ Goal: Correct holotree spaces were created. Wont Have rcc.user Goal: Can get plan for used environment. - Step build/rcc holotree plan 4e67cd8d4_c6880905 + Step build/rcc holotree plan 4e67cd8_c6880905 Must Have micromamba plan Must Have pip plan Must Have post install plan From 7aa541cd2d03f1514649b3c982d6b9757d951944 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 13 Apr 2022 16:42:09 +0300 Subject: [PATCH 245/516] Holotree relocation revisited (v11.10.2) - made presence of hololib.zip more visible on environment creation - test changed so that new holotree relocation can be tested --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 19 +++++++++++++------ pretty/functions.go | 5 +++++ robot_tests/export_holozip.robot | 15 ++------------- robot_tests/fullrun.robot | 7 +++++-- robot_tests/holotree.robot | 7 +++++-- robot_tests/resources.robot | 6 ++++++ 8 files changed, 42 insertions(+), 24 deletions(-) diff --git a/common/version.go b/common/version.go index a51ff693..7d3a4773 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.1` + Version = `v11.10.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index d3186be3..b84645be 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.10.2 (date: 13.4.2022) UNSTABLE + +- made presence of hololib.zip more visible on environment creation +- test changed so that new holotree relocation can be tested + ## v11.10.1 (date: 12.4.2022) UNSTABLE - adding support for user identity in relocation diff --git a/htfs/commands.go b/htfs/commands.go index e344f11b..fd398096 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -11,6 +11,7 @@ import ( "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" "github.com/robocorp/rcc/xviper" ) @@ -18,7 +19,17 @@ import ( func NewEnvironment(condafile, holozip string, restore, force bool) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) - defer common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) + haszip := len(holozip) > 0 + if haszip { + common.Debug("New zipped environment from %q!", holozip) + } + + defer func() { + common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) + if haszip { + pretty.Note("There is hololib.zip present at: %q", holozip) + } + }() common.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) callback := pathlib.LockWaitMessage("Serialized environment creation") @@ -27,11 +38,6 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() - haszip := len(holozip) > 0 - if haszip { - common.Debug("New zipped environment from %q!", holozip) - } - _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash = BlueprintHash(holotreeBlueprint) @@ -65,6 +71,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin } else { common.Progress(12, "Restoring space skipped.") } + return path, scorecard, nil } diff --git a/pretty/functions.go b/pretty/functions.go index 117d3a1d..55b9f4dd 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -11,6 +11,11 @@ func Ok() error { return nil } +func Note(format string, rest ...interface{}) { + niceform := fmt.Sprintf("%s%sNote: %s%s", Cyan, Bold, format, Reset) + common.Log(niceform, rest...) +} + func Warning(format string, rest ...interface{}) { niceform := fmt.Sprintf("%sWarning: %s%s", Yellow, format, Reset) common.Log(niceform, rest...) diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 45be7b5a..8cf801f5 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -11,6 +11,7 @@ Export setup Remove Directory tmp/guest True Remove Directory tmp/standalone True Set Environment Variable ROBOCORP_HOME tmp/developer + Fire And Forget build/rcc ht delete 4e67cd8 Export teardown Set Environment Variable ROBOCORP_HOME tmp/robocorp @@ -31,7 +32,6 @@ Goal: Create environment for standalone robot Must Have RCC_INSTALLATION_ID= Must Have 4e67cd8_fcb4b859 Use STDERR - Must Have Downloading micromamba Must Have Progress: 01/13 Must Have Progress: 13/13 @@ -79,23 +79,12 @@ Goal: Can delete author space Wont Have guest Goal: Can run as guest + Fire And Forget build/rcc ht delete 4e67cd8 Set Environment Variable ROBOCORP_HOME tmp/guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR - Wont Have Downloading micromamba Must Have OK. -Goal: No spaces created under guest - Set Environment Variable ROBOCORP_HOME tmp/guest - Step build/rcc ht ls - Use STDERR - Wont Have 4e67cd8_fcb4b859 - Wont Have rcc.citests - Wont Have author - Wont Have 55aacd3b136421fd - Wont Have 4e67cd8_559e19be - Wont Have guest - Goal: Space created under author for guest Set Environment Variable ROBOCORP_HOME tmp/developer Step build/rcc ht ls diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 9467d672..bd077a7b 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -2,6 +2,11 @@ Library OperatingSystem Library supporting.py Resource resources.robot +Suite Setup Fullrun setup + +*** Keywords *** +Fullrun setup + Fire And Forget build/rcc ht delete 4e67cd8 *** Test cases *** @@ -79,7 +84,6 @@ Goal: Run task in place in debug mode and with timeline. Use STDERR Must Have Progress: 01/13 Must Have Progress: 02/13 - Must Have Progress: 03/13 Must Have Progress: 13/13 Must Have rpaframework Must Have PID # @@ -89,7 +93,6 @@ Goal: Run task in place in debug mode and with timeline. Wont Have Running against old environment Wont Have WARNING Wont Have NOT pristine - Must Have Golden EE file at: Must Have Installation plan is: Must Have Command line is: [ Must Have rcc timeline diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 5829dce1..cae95276 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -2,6 +2,11 @@ Library OperatingSystem Library supporting.py Resource resources.robot +Suite Setup Holotree setup + +*** Keywords *** +Holotree setup + Fire And Forget build/rcc ht delete 4e67cd8 *** Test cases *** @@ -121,9 +126,7 @@ Goal: Liveonly works and uses virtual holotree Goal: Do quick cleanup on environments Step build/rcc config cleanup --controller citests --quick Must Exist %{ROBOCORP_HOME}/bin/micromamba - Must Exist %{ROBOCORP_HOME}/hololib/ Must Exist %{ROBOCORP_HOME}/pkgs/ - Wont Exist %{ROBOCORP_HOME}/holotree/ Wont Exist %{ROBOCORP_HOME}/pipcache/ Use STDERR Must Have OK diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 2ee3fe47..1f8405eb 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -21,6 +21,12 @@ Prepare Local Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ +Fire And Forget + [Arguments] ${command} + ${code} ${output} ${error}= Run and return code output error ${command} + Log STDOUT
${output}
html=yes + Log STDERR
${error}
html=yes + Step [Arguments] ${command} ${expected}=0 ${code} ${output} ${error}= Run and return code output error ${command} From 93e6b3fa131a49ea50e60d3cea94a02ae18f672a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 19 Apr 2022 14:02:20 +0300 Subject: [PATCH 246/516] BUGFIX: various bug fixes (v11.10.3) - fixed panic when settings.yaml is broken, now it will be blunt fatal failure - fixed search path problem with preRunScripts (now robot.yaml PATH is used) - removed direct mentions of Robocorp App (old name) --- cmd/assistant.go | 2 +- cmd/robot.go | 2 +- cmd/task.go | 2 +- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/running.go | 23 ++++++++++++++--------- settings/settings.go | 11 +++++++++++ templates/extended/README.md | 2 +- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/cmd/assistant.go b/cmd/assistant.go index 0d8aa63c..6f8423d3 100644 --- a/cmd/assistant.go +++ b/cmd/assistant.go @@ -9,7 +9,7 @@ var assistantCmd = &cobra.Command{ Aliases: []string{"assist", "a"}, Short: "Group of commands related to `robot assistant`.", Long: `This set of commands relate to Robocorp Robot Assistant related tasks. -They are either local, or in relation to Robocorp Control Room and Robocorp App.`, +They are either local, or in relation to Robocorp Control Room and tooling.`, } func init() { diff --git a/cmd/robot.go b/cmd/robot.go index cb484f66..69d6c5c6 100644 --- a/cmd/robot.go +++ b/cmd/robot.go @@ -9,7 +9,7 @@ var robotCmd = &cobra.Command{ Aliases: []string{"r"}, Short: "Group of commands related to `robot`.", Long: `This set of commands relate to Robocorp Control Room related tasks. They are -executed either locally, or in connection to Robocorp Control Room and Robocorp App.`, +executed either locally, or in connection to Robocorp Control Room and tooling.`, } func init() { diff --git a/cmd/task.go b/cmd/task.go index 27ba4bb2..e4fbb04e 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -9,7 +9,7 @@ var taskCmd = &cobra.Command{ Aliases: []string{"t"}, Short: "Group of commands related to `task`.", Long: `This set of commands relate to Robocorp Control Room related tasks. They are -executed either locally, or in connection to Robocorp Control Room and Robocorp App.`, +executed either locally, or in connection to Robocorp Control Room and tooling.`, } func init() { diff --git a/common/version.go b/common/version.go index 7d3a4773..2e24cc9b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.2` + Version = `v11.10.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index b84645be..3893238c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.10.3 (date: 19.4.2022) + +- fixed panic when settings.yaml is broken, now it will be blunt fatal failure +- fixed search path problem with preRunScripts (now robot.yaml PATH is used) +- removed direct mentions of Robocorp App (old name) + ## v11.10.2 (date: 13.4.2022) UNSTABLE - made presence of hololib.zip more visible on environment creation diff --git a/operations/running.go b/operations/running.go index 715edead..83718840 100644 --- a/operations/running.go +++ b/operations/running.go @@ -190,6 +190,18 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t pretty.Ok() } +func findExecutableOrDie(searchPath pathlib.PathParts, executable string) string { + found, ok := searchPath.Which(executable, conda.FileExtensions) + if !ok { + pretty.Exit(6, "Error: Cannot find command: %v", executable) + } + fullpath, err := filepath.EvalSymlinks(found) + if err != nil { + pretty.Exit(7, "Error: %v", err) + } + return fullpath +} + func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { common.Debug("Command line is: %v", template) developmentEnvironment, err := robot.LoadEnvironmentSetup(flags.EnvironmentFile) @@ -199,14 +211,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro task := make([]string, len(template)) copy(task, template) searchPath := config.SearchPath(label) - found, ok := searchPath.Which(task[0], conda.FileExtensions) - if !ok { - pretty.Exit(6, "Error: Cannot find command: %v", task[0]) - } - fullpath, err := filepath.EvalSymlinks(found) - if err != nil { - pretty.Exit(7, "Error: %v", err) - } + task[0] = findExecutableOrDie(searchPath, task[0]) var data Token if !flags.Assistant && len(flags.WorkspaceId) > 0 { claims := RunRobotClaims(flags.ValidityTime*60, flags.WorkspaceId) @@ -215,7 +220,6 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if err != nil { pretty.Exit(8, "Error: %v", err) } - task[0] = fullpath directory := config.WorkingDirectory() environment := config.RobotExecutionEnvironment(label, developmentEnvironment.AsEnvironment(), true) if len(data) > 0 { @@ -254,6 +258,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if err != nil { pretty.Exit(11, "%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) } + scriptCommand[0] = findExecutableOrDie(searchPath, scriptCommand[0]) common.Debug("Running pre run script '%s' ...", script) _, err = shell.New(environment, directory, scriptCommand...).Execute(interactive) if err != nil { diff --git a/settings/settings.go b/settings/settings.go index 6f433cb5..5fe2c21d 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -258,7 +258,18 @@ func (it gateway) loadRootCAs() *x509.CertPool { return roots } +func initProtection() { + status := recover() + if status != nil { + fmt.Fprintf(os.Stderr, "Fatal failure with '%v', see: %v\n", common.SettingsFile(), status) + fmt.Fprintln(os.Stderr, "Sorry. Cannot recover, will exit now!") + os.Exit(111) + } +} + func init() { + defer initProtection() + chain = SettingsLayers{ DefaultSettingsLayer(), CustomSettingsLayer(), diff --git a/templates/extended/README.md b/templates/extended/README.md index 4a7d5552..262c1952 100644 --- a/templates/extended/README.md +++ b/templates/extended/README.md @@ -58,7 +58,7 @@ See [Docs](https://robocorp.com/docs/development-howtos/variables-and-secrets/) Give the task name and startup commands in `robot.yaml` with some additional configuration. See [Docs](https://robocorp.com/docs/setup/robot-structure#robot-configuration-file-robot-yaml) for more. -Put all the robot dependencies in `conda.yaml`. Robocorp App (and rcc) uses [Conda](https://docs.conda.io) for managing the execution environment. For development you can also install packages manually with `pip`. +Put all the robot dependencies in `conda.yaml`. Robocorp tools (and rcc) uses [Conda](https://docs.conda.io) for managing the execution environment. For development you can also install packages manually with `pip`. ### Additional documentation See [Robocorp Docs](https://robocorp.com/docs/) for more documentation. From f364f0a7a90b9d07710589e746fa1a1e24ce2b2f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Apr 2022 15:12:51 +0300 Subject: [PATCH 247/516] FEATURE/FIX: preRunScripts limits (v11.10.4) - different preRunScripts for different operating systems - acceptable differentiation patterns are: amd64/arm64/darwin/windows/linux --- common/version.go | 2 +- docs/changelog.md | 5 +++++ docs/recipes.md | 5 +++++ operations/running.go | 4 ++++ robot/robot.go | 9 +++++---- robot/robot_test.go | 37 +++++++++++++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 2e24cc9b..8e3e1f47 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.3` + Version = `v11.10.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3893238c..95b41faa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.10.4 (date: 20.4.2022) + +- different preRunScripts for different operating systems +- acceptable differentiation patterns are: amd64/arm64/darwin/windows/linux + ## v11.10.3 (date: 19.4.2022) - fixed panic when settings.yaml is broken, now it will be blunt fatal failure diff --git a/docs/recipes.md b/docs/recipes.md index 4680a07a..7690ff9b 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -485,6 +485,11 @@ execution. Idea with these scripts is that they can be used to customize runtime environment right after it has been restored from hololib, and just before actual robot execution is done. +If script names contains some of "amd64", "arm64", "darwin", "windows" and/or +"linux" words (like `script_for_amd64_linux.sh`) then other architectures +and operating systems will skip those scripts, and only amd64 linux systems +will execute them. + All these scripts are run in "robot" context with all same environment variables available as in robot run. diff --git a/operations/running.go b/operations/running.go index 83718840..ebb85c8d 100644 --- a/operations/running.go +++ b/operations/running.go @@ -3,6 +3,7 @@ package operations import ( "fmt" "path/filepath" + "runtime" "strings" "github.com/google/shlex" @@ -254,6 +255,9 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if preRunScripts != nil && len(preRunScripts) > 0 { common.Debug("=== pre run script phase ===") for _, script := range preRunScripts { + if !robot.PlatformAcceptableFile(runtime.GOARCH, runtime.GOOS, script) { + continue + } scriptCommand, err := shlex.Split(script) if err != nil { pretty.Exit(11, "%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) diff --git a/robot/robot.go b/robot/robot.go index 60da7cfc..88361995 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -348,6 +348,10 @@ func submatch(pattern *regexp.Regexp, expected, text string) bool { return match == nil || len(match) == 0 || match[0] == expected } +func PlatformAcceptableFile(architecture, operatingSystem, filename string) bool { + return submatch(GoarchPattern, architecture, filename) && submatch(GoosPattern, operatingSystem, filename) +} + func (it *robot) availableEnvironmentConfigurations(marker string) []string { result := make([]string, 0, len(it.Environments)) common.Trace("Available environment configurations:") @@ -358,10 +362,7 @@ func (it *robot) availableEnvironmentConfigurations(marker string) []string { if (underscored || freezed) && !marked { continue } - if !submatch(GoosPattern, runtime.GOOS, part) { - continue - } - if !submatch(GoarchPattern, runtime.GOARCH, part) { + if !PlatformAcceptableFile(runtime.GOARCH, runtime.GOOS, part) { continue } fullpath := filepath.Join(it.Root, part) diff --git a/robot/robot_test.go b/robot/robot_test.go index b413dd4d..fa0afc41 100644 --- a/robot/robot_test.go +++ b/robot/robot_test.go @@ -16,6 +16,43 @@ func TestCannotReadMissingRobotYaml(t *testing.T) { must.Nil(sut) } +func TestCanAcceptPlatformFiles(t *testing.T) { + must, wont := hamlet.Specifications(t) + + for _, filename := range []string{"anyscript", "anyscript.bat", "anyscript.cmd"} { + must.True(robot.PlatformAcceptableFile("amd64", "linux", filename)) + must.True(robot.PlatformAcceptableFile("amd64", "windows", filename)) + must.True(robot.PlatformAcceptableFile("amd64", "darwin", filename)) + must.True(robot.PlatformAcceptableFile("arm64", "linux", filename)) + must.True(robot.PlatformAcceptableFile("arm64", "windows", filename)) + must.True(robot.PlatformAcceptableFile("arm64", "darwin", filename)) + } + + for _, filename := range []string{"any.bat", "any.cmd", "any.sh", "at_linux.sh", "at_arm64.sh", "at_arm64_linux.sh"} { + must.True(robot.PlatformAcceptableFile("arm64", "linux", filename)) + } + + for _, filename := range []string{"any.bat", "any.cmd", "any.sh", "at_darwin.sh", "at_arm64.sh", "at_arm64_darwin.sh"} { + must.True(robot.PlatformAcceptableFile("arm64", "darwin", filename)) + } + + for _, filename := range []string{"any.bat", "any.cmd", "any.sh", "at_windows.sh", "at_amd64.sh", "at_amd64_windows.sh"} { + must.True(robot.PlatformAcceptableFile("amd64", "windows", filename)) + } + + for _, filename := range []string{"at_arm64.sh", "at_windows.bat", "at_darwin.sh", "at_arm64_linux.sh"} { + wont.True(robot.PlatformAcceptableFile("amd64", "linux", filename)) + } + + for _, filename := range []string{"at_linux.sh", "at_arm64.sh", "at_amd64_darwin.sh", "at_amd64_linux.sh", "at_arm64_windows.sh"} { + wont.True(robot.PlatformAcceptableFile("amd64", "windows", filename)) + } + + for _, filename := range []string{"at_linux.sh", "at_arm64.sh", "at_arm64_darwin.sh", "at_amd64_linux.sh", "at_amd64_windows.sh"} { + wont.True(robot.PlatformAcceptableFile("amd64", "darwin", filename)) + } +} + func TestCanMatchArchitecture(t *testing.T) { must, wont := hamlet.Specifications(t) From 1ccd9bedffa2cf768e5b2364f9e3158202b12990 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Apr 2022 16:50:01 +0300 Subject: [PATCH 248/516] FEATURE/FIX: CA-bundle visible in settings (v11.10.5) - settings certificates section now has full path to CA bundle if available --- common/version.go | 2 +- docs/changelog.md | 4 ++++ settings/data.go | 9 +++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 8e3e1f47..bec0e6da 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.4` + Version = `v11.10.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 95b41faa..2352d0f1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.10.5 (date: 20.4.2022) + +- settings certificates section now has full path to CA bundle if available + ## v11.10.4 (date: 20.4.2022) - different preRunScripts for different operating systems diff --git a/settings/data.go b/settings/data.go index b0179ac1..2dc676e6 100644 --- a/settings/data.go +++ b/settings/data.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "gopkg.in/yaml.v1" ) @@ -224,8 +225,9 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { } type Certificates struct { - VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` - SslNoRevoke bool `yaml:"ssl-no-revoke" json:"ssl-no-revoke"` + VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` + SslNoRevoke bool `yaml:"ssl-no-revoke" json:"ssl-no-revoke"` + CaBundle string `yaml:"ca-bundle,omitempty" json:"ca-bundle,omitempty"` } func (it *Certificates) onTopOf(target *Settings) { @@ -234,6 +236,9 @@ func (it *Certificates) onTopOf(target *Settings) { } target.Certificates.VerifySsl = it.VerifySsl target.Certificates.SslNoRevoke = it.SslNoRevoke + if pathlib.IsFile(common.CaBundleFile()) { + target.Certificates.CaBundle = common.CaBundleFile() + } } type Endpoints struct { From 7e836c5dbed729807ef072bc41c352b81d985248 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 21 Apr 2022 12:15:39 +0300 Subject: [PATCH 249/516] BUGFIX: shared lock files (v11.10.6) - bugfix: lock files are now marked as shared files - Rakefile: using `go install` for bindata from now on --- Rakefile | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++++ pathlib/functions.go | 8 ++++++++ pathlib/lock_unix.go | 3 ++- pathlib/lock_windows.go | 3 ++- 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Rakefile b/Rakefile index 075fd092..39bd361d 100644 --- a/Rakefile +++ b/Rakefile @@ -16,7 +16,7 @@ task :tooling do puts "PATH is #{ENV['PATH']}" puts "GOPATH is #{ENV['GOPATH']}" puts "GOROOT is #{ENV['GOROOT']}" - sh "go get -u github.com/go-bindata/go-bindata/..." + sh "go install github.com/go-bindata/go-bindata/..." sh "which -a zip || echo NA" sh "which -a go-bindata || echo NA" sh "ls -l $HOME/go/bin" diff --git a/common/version.go b/common/version.go index bec0e6da..c38cd4c4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.5` + Version = `v11.10.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2352d0f1..11f4cb7a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.10.6 (date: 21.4.2022) + +- bugfix: lock files are now marked as shared files +- Rakefile: using `go install` for bindata from now on + ## v11.10.5 (date: 20.4.2022) - settings certificates section now has full path to CA bundle if available diff --git a/pathlib/functions.go b/pathlib/functions.go index 9ab2f2fa..382ba249 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -109,6 +109,14 @@ func doEnsureDirectory(directory string, mode fs.FileMode) (string, error) { return fullpath, nil } +func EnsureSharedDirectory(directory string) (string, error) { + return MakeSharedDir(directory) +} + +func EnsureSharedParentDirectory(resource string) (string, error) { + return EnsureSharedDirectory(filepath.Dir(resource)) +} + func EnsureDirectory(directory string) (string, error) { return doEnsureDirectory(directory, 0o750) } diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index c89dab84..06036115 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -18,7 +18,7 @@ func Locker(filename string, trycount int) (Releaser, error) { defer common.Stopwatch("LOCKER: Got lock on %v in", filename).Report() } common.Trace("LOCKER: Want lock on: %v", filename) - _, err := EnsureParentDirectory(filename) + _, err := EnsureSharedParentDirectory(filename) if err != nil { return nil, err } @@ -26,6 +26,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } + defer MakeSharedFile(filename) err = syscall.Flock(int(file.Fd()), int(syscall.LOCK_EX)) if err != nil { return nil, err diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index cdad24f8..a329b470 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -40,7 +40,7 @@ func Locker(filename string, trycount int) (Releaser, error) { }() } common.Trace("LOCKER: Want lock on: %v", filename) - _, err = EnsureParentDirectory(filename) + _, err = EnsureSharedParentDirectory(filename) if err != nil { return nil, err } @@ -55,6 +55,7 @@ func Locker(filename string, trycount int) (Releaser, error) { continue } break + MakeSharedFile(filename) } for { trycount -= 1 From 4724b10c868f48922da3c8e1a4836d1071c43753 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 22 Apr 2022 10:41:28 +0300 Subject: [PATCH 250/516] BUGFIX: shared lock files again (v11.10.7) - bugfix/retry: lock files are now marked as shared files (actually this will not work on Windows on multi-user setup) - changed robot test setup cleanup --- cmd/script.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ docs/usecases.md | 4 +++- pathlib/lock_unix.go | 5 ++++- pathlib/lock_windows.go | 5 ++++- robot_tests/resources.robot | 2 ++ 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cmd/script.go b/cmd/script.go index bc4092f5..29dea180 100644 --- a/cmd/script.go +++ b/cmd/script.go @@ -9,8 +9,8 @@ import ( var scriptCmd = &cobra.Command{ Use: "script", - Short: "Run script inside robot task envrionment.", - Long: "Run script inside robot task envrionment.", + Short: "Run script inside robot task environment.", + Long: "Run script inside robot task environment.", Example: ` rcc task script -- pip list rcc task script --silent -- python --version diff --git a/common/version.go b/common/version.go index c38cd4c4..e7711956 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.6` + Version = `v11.10.7` ) diff --git a/docs/changelog.md b/docs/changelog.md index 11f4cb7a..301d7a25 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.10.7 (date: 22.4.2022) + +- bugfix/retry: lock files are now marked as shared files (actually this + will not work on Windows on multi-user setup) +- changed robot test setup cleanup + ## v11.10.6 (date: 21.4.2022) - bugfix: lock files are now marked as shared files diff --git a/docs/usecases.md b/docs/usecases.md index a8156341..d221f626 100644 --- a/docs/usecases.md +++ b/docs/usecases.md @@ -6,6 +6,7 @@ communicate to Robocorp Control Room * provide commands that can be used in CI pipelines (Jenkins, Gitlab CI, ...) to push robots into Robocorp Control Room +* can also be used to run robot tests in CI/CD environments * provide isolated environments to run python scripts and applications * to use other scripting languages and tools available from conda-forge (or conda in general) with isolated and easily installed manner (see list below @@ -23,10 +24,11 @@ * r and libraries * julia and libraries * make, cmake and compilers (C++, Fortran, ...) -* nginx * nodejs +* nginx * rust * php +* go * gawk, sed, and emacs, vim * ROS libraries (robot operating system) * firefox diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 06036115..d3e9959b 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -26,7 +26,10 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } - defer MakeSharedFile(filename) + _, err = MakeSharedFile(filename) + if err != nil { + return nil, err + } err = syscall.Flock(int(file.Fd()), int(syscall.LOCK_EX)) if err != nil { return nil, err diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index a329b470..1d56dcd7 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -54,8 +54,11 @@ func Locker(filename string, trycount int) (Releaser, error) { time.Sleep(40 * time.Millisecond) continue } + _, err = MakeSharedFile(filename) + if err != nil { + return nil, err + } break - MakeSharedFile(filename) } for { trycount -= 1 diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 1f8405eb..dc91f6a7 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -15,6 +15,8 @@ Prepare Local Create Directory tmp/robocorp Set Environment Variable ROBOCORP_HOME tmp/robocorp + Fire And Forget build/rcc ht delete 4e67cd8 + Comment Verify micromamba is installed or download and install it. Step build/rcc ht vars --controller citests robot_tests/conda.yaml Must Exist %{ROBOCORP_HOME}/bin/ From 3044ca3ead3ef8f7349151f2c6018b6e25c0f002 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 25 Apr 2022 12:29:45 +0300 Subject: [PATCH 251/516] FEATURE: dev tasks (v11.11.0) - in addition to normal tasks, now robot.yaml can also contain devTasks - it is activated with flag `--dev` and only available in task run command --- cmd/run.go | 1 + common/variables.go | 1 + common/version.go | 2 +- docs/changelog.md | 5 +++++ docs/recipes.md | 17 +++++++++++++++++ operations/running.go | 2 +- robot/robot.go | 31 +++++++++++++++++++++++++------ 7 files changed, 51 insertions(+), 8 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 1f6e0ccd..5ba04bc5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -59,4 +59,5 @@ func init() { runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") + runCmd.Flags().BoolVarP(&common.DeveloperFlag, "dev", "", false, "Use devTasks instead of normal tasks. For development work only. Stragegy selection.") } diff --git a/common/variables.go b/common/variables.go index 5827e9ba..4a9f1ecd 100644 --- a/common/variables.go +++ b/common/variables.go @@ -23,6 +23,7 @@ var ( Silent bool DebugFlag bool TraceFlag bool + DeveloperFlag bool StrictFlag bool LogLinenumbers bool NoCache bool diff --git a/common/version.go b/common/version.go index e7711956..0b69a64e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.10.7` + Version = `v11.11.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 301d7a25..847aaf39 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.11.0 (date: 25.4.2022) + +- in addition to normal tasks, now robot.yaml can also contain devTasks +- it is activated with flag `--dev` and only available in task run command + ## v11.10.7 (date: 22.4.2022) - bugfix/retry: lock files are now marked as shared files (actually this diff --git a/docs/recipes.md b/docs/recipes.md index 7690ff9b..bb705abc 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -455,6 +455,23 @@ There are three types of task declarations: arguments, and it is most accurate way to declare CLI form, but it is also most spacious form. +### What are `devTasks:`? + +They are tasks like above `tasks:` define. But they have two major differences +compared to normal `tasks:` definitions: + +1. They are for developers at development machine, for doing development time + activities and tasks. They should never be available in cloud containers, + Assistants or Agents. Developer tools can provide support for them, but + their semantics should be only valid in development context. +2. They can be run like normal tasks, by providing `--dev` flag. But durng + their run, all `preRunScripts:` are ignored. Otherwise environment is + created and managed as normal tasks, but without pre-run scripts applied. + +Their primary goal is provide developers way to use same tooling to automate +their development process, like normal `tasks:` provide ways to automate +robot actions. + ### What is `condaConfigFile:`? This is actual name used as `conda.yaml` environment configuration file. diff --git a/operations/running.go b/operations/running.go index ebb85c8d..ba6a9ff7 100644 --- a/operations/running.go +++ b/operations/running.go @@ -252,7 +252,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro FreezeEnvironmentListing(label, config) preRunScripts := config.PreRunScripts() - if preRunScripts != nil && len(preRunScripts) > 0 { + if !common.DeveloperFlag && preRunScripts != nil && len(preRunScripts) > 0 { common.Debug("=== pre run script phase ===") for _, script := range preRunScripts { if !robot.PlatformAcceptableFile(runtime.GOARCH, runtime.GOOS, script) { diff --git a/robot/robot.go b/robot/robot.go index 88361995..02a46b3a 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -14,6 +14,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/google/shlex" "gopkg.in/yaml.v2" @@ -54,6 +55,7 @@ type Task interface { type robot struct { Tasks map[string]*task `yaml:"tasks"` + Devtasks map[string]*task `yaml:"devTasks"` Conda string `yaml:"condaConfigFile,omitempty"` PreRun []string `yaml:"preRunScripts,omitempty"` Environments []string `yaml:"environmentConfigs,omitempty"` @@ -71,12 +73,26 @@ type task struct { robot *robot } +func (it *robot) taskMap() map[string]*task { + if common.DeveloperFlag { + pretty.Note("Operating in developer mode. Using 'devTasks:' instead of 'tasks:'.") + return it.Devtasks + } else { + return it.Tasks + } +} + func (it *robot) relink() { for _, task := range it.Tasks { if task != nil { task.robot = it } } + for _, task := range it.Devtasks { + if task != nil { + task.robot = it + } + } } func (it *robot) diagnoseTasks(diagnose common.Diagnoser) { @@ -281,8 +297,9 @@ func (it *robot) IgnoreFiles() []string { } func (it *robot) AvailableTasks() []string { - result := make([]string, 0, len(it.Tasks)) - for name, _ := range it.Tasks { + tasks := it.taskMap() + result := make([]string, 0, len(tasks)) + for name, _ := range tasks { result = append(result, fmt.Sprintf("%q", name)) } sort.Strings(result) @@ -290,11 +307,12 @@ func (it *robot) AvailableTasks() []string { } func (it *robot) DefaultTask() Task { - if len(it.Tasks) != 1 { + tasks := it.taskMap() + if len(tasks) != 1 { return nil } var result *task - for _, value := range it.Tasks { + for _, value := range tasks { result = value break } @@ -305,13 +323,14 @@ func (it *robot) TaskByName(name string) Task { if len(name) == 0 { return it.DefaultTask() } + tasks := it.taskMap() key := strings.TrimSpace(name) - found, ok := it.Tasks[key] + found, ok := tasks[key] if ok { return found } caseless := strings.ToLower(key) - for name, value := range it.Tasks { + for name, value := range tasks { if caseless == strings.ToLower(strings.TrimSpace(name)) { return value } From a2072d039b5635d2f87300c748f768f8fdad9a9d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 26 Apr 2022 12:35:31 +0300 Subject: [PATCH 252/516] BUGFIX: hololib catalog v12 identification (v11.11.1) - bugfix: added v12 indicator in new form of holotree catalogs (to separate from old ones) to allow old and new versions to co-exist - documentation update for devTasks and ToC --- common/version.go | 2 +- docs/README.md | 17 +++++++++-------- docs/changelog.md | 6 ++++++ docs/recipes.md | 21 +++++++++++++++++---- htfs/library.go | 4 ++-- htfs/testdata/simple.zip | Bin 154134 -> 158614 bytes htfs/ziplibrary.go | 2 +- scripts/toc.py | 2 +- 8 files changed, 37 insertions(+), 17 deletions(-) diff --git a/common/version.go b/common/version.go index 0b69a64e..2bd4f3d0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.11.0` + Version = `v11.11.1` ) diff --git a/docs/README.md b/docs/README.md index cb07a260..e714e435 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# ToC for rcc documentation +# Table of contents: rcc documentation ## 1 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases) ### 1.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge) ## 2 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features) @@ -32,13 +32,14 @@ #### 3.10.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) #### 3.10.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) #### 3.10.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.10.4 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.10.5 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.10.6 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.10.7 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.10.8 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.10.9 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.10.10 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +#### 3.10.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.10.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.10.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.10.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.10.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.10.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.10.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.10.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) ### 3.11 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) #### 3.11.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) #### 3.11.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) diff --git a/docs/changelog.md b/docs/changelog.md index 847aaf39..af590cb4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.11.1 (date: 26.4.2022) + +- bugfix: added v12 indicator in new form of holotree catalogs (to separate + from old ones) to allow old and new versions to co-exist +- documentation update for devTasks and ToC + ## v11.11.0 (date: 25.4.2022) - in addition to normal tasks, now robot.yaml can also contain devTasks diff --git a/docs/recipes.md b/docs/recipes.md index bb705abc..cbf47fd1 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -396,6 +396,12 @@ tasks: - Task log - tasks.robot +devTasks: + Editor setup: + shell: python scripts/editor_setup.py + Repository update: + shell: python scripts/repository_update.py + condaConfigFile: conda.yaml environmentConfigs: @@ -460,17 +466,24 @@ There are three types of task declarations: They are tasks like above `tasks:` define. But they have two major differences compared to normal `tasks:` definitions: -1. They are for developers at development machine, for doing development time +1. They are for developers at development machines, for doing development time activities and tasks. They should never be available in cloud containers, Assistants or Agents. Developer tools can provide support for them, but their semantics should be only valid in development context. -2. They can be run like normal tasks, by providing `--dev` flag. But durng +2. They can be run like normal tasks, by providing `--dev` flag. But during their run, all `preRunScripts:` are ignored. Otherwise environment is - created and managed as normal tasks, but without pre-run scripts applied. + created and managed as with normal tasks, but without pre-run scripts + applied. Their primary goal is provide developers way to use same tooling to automate their development process, like normal `tasks:` provide ways to automate -robot actions. +robot actions. Some examples could be common editor setups and version control +repository updates. + +Currently `--dev` option is only available in `rcc run` and `rcc task run` +commands. And when that `--dev` option is given on command line, only +`devTasks:` are available for running and all normal `tasks:` are missing. +And when that option is missing, then also `devTasks:` are invisible/missing. ### What is `condaConfigFile:`? diff --git a/htfs/library.go b/htfs/library.go index 094a7ca6..ceed9536 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -186,7 +186,7 @@ func (it *hololib) Record(blueprint []byte) error { } func (it *hololib) CatalogPath(key string) string { - name := fmt.Sprintf("%s.%s", key, common.Platform()) + name := fmt.Sprintf("%sv12.%s", key, common.Platform()) return filepath.Join(common.HololibCatalogLocation(), name) } @@ -229,7 +229,7 @@ func (it *hololib) queryBlueprint(key string) bool { func Catalogs() []string { result := make([]string, 0, 10) - for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*.*") { + for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*v12.*") { result = append(result, catalog) } sort.Strings(result) diff --git a/htfs/testdata/simple.zip b/htfs/testdata/simple.zip index bfba0b7b44e3db7951e16a4b78bd231a26417d43..5f747b1f80448974a052ba0785ad7990b45f1db7 100644 GIT binary patch delta 57410 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 delta 53030 zcmYhgWmFtYv@D9dySo#7U~qQ}P9WGIg9P_rLvVL@C%C&LxVs0J;O@@L_wKsqo?iW@ zclVE8)w`

@@ -12,30 +14,38 @@ RCC is actively maintained by [Robocorp](https://www.robocorp.com/). ## Why use rcc? + +* You do not need to install Python on the target machine +* You can control exactly which version of Python your automation will run on (..and which pip version is used to resolve dependencies) +* You can avoid `Works on my machine` +* No need for `venv`, `pyenv`, ... tooling and knowledge sharing inside your team. +* Define dependencies in `conda.yaml` and automation config in `robot.yaml` and let RCC do the heavy lifting. +* If you have run into "dependency drifts", where once working runtime environment dependencies get updated and break your production system?, RCC can freeze ALL dependencies, pre-build environments, and more. +* RCC will give you a heads-up if your automations have been leaving behind processes after running. -* Are developers manually installing conda or pip packages? Here rcc makes it easier for developers to just worry about getting `conda.yaml` and `robot.yaml` right, and then let rcc to do the heavy lifting of keeping environments pristine, clean, and up to date. -* Have you run into "works on my machine" problem, where the original developer has a working setup, but others have a hard time repeating the experience? In this case, let rcc help you to set up repeatable runtime environments across users and operating systems. -* Have you experienced "configuration drift", where once working runtime environment dependencies get updated and break your production system? Here rcc can help by either making drift visible or freezing all dependencies so that drifting does not happen. -* Do you have python programs that have conflicting dependencies? There rcc can help by making dedicated runtime environments for different setups, where different `robot.yaml` files define what to run and `conda.yaml` defines runtime environment dependencies +...and much much more. +👉 If the command line seems scary, just pick up [Robocorp Code](https://marketplace.visualstudio.com/items?itemName=robocorp.robocorp-code) -extension for VS Code, and you'll get the power of RCC directly in VS Code without worrying about the commands. ## Getting Started :arrow_double_down: Install rcc -> [Install](#installing-rcc-from-command-line) or [Download RCC](#direct-downloads-for-signed-executables-provided-by-robocorp) +> [Installation guide](#installing-rcc-from-command-line) :octocat: Pull robot from GitHub: -> `rcc pull github.com/robocorp/example-google-image-search` +> `rcc pull github.com/robocorp/template-python-browser` :running: Run robot > `rcc run` -:hatching_chick: Create your own robot from template -> `rcc robot initialize -t standard` +:hatching_chick: Create your own robot from templates +> `rcc create` + +For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.com/docs/product-manuals/robocorp-cli) to get started. To build `rcc` from this repository, see the [Setup Guide](/docs/BUILD.md) -For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.com/docs/product-manuals/robocorp-cli) to get started. To build `rcc` from this repository see the [Setup Guide](/docs/BUILD.md) +## Installing RCC from the command line -## Installing RCC from command line +> Links to changelog and different versions [available here](https://downloads.robocorp.com/rcc/releases/index.html) ### Windows @@ -48,19 +58,12 @@ For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.c #### Brew cask from Robocorp tap +1. Update brew: `brew update` 1. Install: `brew install robocorp/tools/rcc` 1. Test: `rcc` Upgrading: `brew upgrade rcc` -#### Raw download - -1. Open the terminal -1. Download: `curl -o rcc https://downloads.robocorp.com/rcc/releases/latest/macos64/rcc` -1. Make the downloaded file executable: `chmod a+x rcc` -1. Add to path: `sudo mv rcc /usr/local/bin/` -1. Test: `rcc` - ### Linux 1. Open the terminal @@ -69,19 +72,16 @@ Upgrading: `brew upgrade rcc` 1. Add to path: `sudo mv rcc /usr/local/bin/` 1. Test: `rcc` -### [Direct downloads for signed executables provided by Robocorp](https://downloads.robocorp.com/rcc/releases/index.html) - -Follow above link to download site. Both tested and bleeding edge versions are available from same location. - -*[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf)* - ## Documentation Visit [https://robocorp.com/docs](https://robocorp.com/docs) to view the full documentation on the full Robocorp stack. -Changelog can be seen [here.](/docs/changelog.md) It is also visible inside rcc using command `rcc docs changelog`. +The changelog can be seen [here](/docs/changelog.md). It is also visible inside RCC using the command `rcc docs changelog`. + +[EULA for pre-built distribution.](https://cdn.robocorp.com/legal/Robocorp-EULA-v1.0.pdf) -Some tips, tricks, and recipes can be found [here.](/docs/recipes.md) They are also visible inside rcc using command `rcc docs recipes`. +Some tips, tricks, and recipes can be found [here](/docs/recipes.md). +These are also visible inside RCC using the command: `rcc docs recipes`. ## Community and Support From 1d18ab98177499c1ee10a0264bdc35bd578314e4 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Tue, 9 Jan 2024 17:13:10 +0200 Subject: [PATCH 467/516] Fixed link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad0bf75e..5742480a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ RCC is actively maintained by [Robocorp](https://www.robocorp.com/). ## Getting Started :arrow_double_down: Install rcc -> [Installation guide](#installing-rcc-from-command-line) +> [Installation guide](https://github.com/robocorp/rcc?tab=readme-ov-file#installing-rcc-from-the-command-line) :octocat: Pull robot from GitHub: > `rcc pull github.com/robocorp/template-python-browser` From 4c0fe17003d7be413a015d4fd2238751f24de2a0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Jan 2024 12:15:05 +0200 Subject: [PATCH 468/516] Security: remove ECC experiment (v17.13.0) --- cmd/assistantRun.go | 7 +-- cmd/internale2ee.go | 94 ------------------------------------- common/version.go | 2 +- docs/changelog.md | 5 ++ go.mod | 7 ++- go.sum | 4 -- operations/assistant.go | 9 +--- operations/encryptionv2.go | 78 ------------------------------ operations/security_test.go | 31 ------------ 9 files changed, 14 insertions(+), 223 deletions(-) delete mode 100644 cmd/internale2ee.go delete mode 100644 operations/encryptionv2.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 0c59d19f..e5735014 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -49,7 +49,7 @@ var assistantRunCmd = &cobra.Command{ cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) }() common.Timeline("start assistant run cloud call started") - assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId, useEcc) + assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId) common.Timeline("start assistant run cloud call completed") if err != nil { pretty.Exit(3, "Could not run assistant, reason: %v", err) @@ -123,10 +123,6 @@ var assistantRunCmd = &cobra.Command{ }, } -var ( - useEcc bool -) - func init() { assistantCmd.AddCommand(assistantRunCmd) assistantRunCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to get assistant information.") @@ -134,6 +130,5 @@ func init() { assistantRunCmd.Flags().StringVarP(&assistantId, "assistant", "a", "", "Assistant id to execute.") assistantRunCmd.MarkFlagRequired("assistant") assistantRunCmd.Flags().StringVarP(©Directory, "copy", "c", "", "Location to copy changed artifacts from run (optional).") - assistantRunCmd.Flags().BoolVarP(&useEcc, "ecc", "", false, "DO NOT USE! INTERNAL EXPERIMENT!") assistantRunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") } diff --git a/cmd/internale2ee.go b/cmd/internale2ee.go deleted file mode 100644 index a0a58f33..00000000 --- a/cmd/internale2ee.go +++ /dev/null @@ -1,94 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/robocorp/rcc/cloud" - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - encryptionVersion int -) - -var e2eeCmd = &cobra.Command{ - Use: "encryption", - Short: "Internal end-to-end encryption tester method", - Long: "Internal end-to-end encryption tester method", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Encryption lasted").Report() - } - if encryptionVersion == 1 { - version1encryption(args) - } else { - version2encryption(args) - } - pretty.Ok() - }, -} - -func version1encryption(args []string) { - account := operations.AccountByName(AccountName()) - pretty.Guard(account != nil, 1, "Could not find account by name: %q", AccountName()) - - client, err := cloud.NewClient(account.Endpoint) - pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) - - key, err := operations.GenerateEphemeralKey() - pretty.Guard(err == nil, 3, "Problem with key generation, reason: %v", err) - - request := client.NewRequest("/assistant-v1/test/encryption") - request.Body, err = key.RequestBody(args[0]) - pretty.Guard(err == nil, 4, "Problem with body generation, reason: %v", err) - - response := client.Post(request) - pretty.Guard(response.Status == 200, 5, "Problem with test request, status=%d, body=%s", response.Status, response.Body) - - plaintext, err := key.Decode(response.Body) - pretty.Guard(err == nil, 6, "Decode problem with body %s, reason: %v", response.Body, err) - - common.Log("Response: %s", string(plaintext)) -} - -func version2encryption(args []string) { - account := operations.AccountByName(AccountName()) - pretty.Guard(account != nil, 1, "Could not find account by name: %q", AccountName()) - - client, err := cloud.NewClient(account.Endpoint) - pretty.Guard(err == nil, 2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) - - key, err := operations.GenerateEphemeralEccKey() - pretty.Guard(err == nil, 3, "Problem with key generation, reason: %v", err) - - location := fmt.Sprintf("/assistant-v1/workspaces/%s/assistants/%s/test", workspaceId, assistantId) - request := client.NewRequest(location) - request.Headers["Authorization"] = fmt.Sprintf("RC-WSKEY %s", wskey) - request.Body, err = key.RequestBody(nil) - pretty.Guard(err == nil, 4, "Problem with body generation, reason: %v", err) - - common.Timeline("POST to cloud started") - response := client.Post(request) - common.Timeline("POST done") - pretty.Guard(response.Status == 200, 5, "Problem with test request, status=%d, body=%s", response.Status, response.Body) - - common.Timeline("decode start") - plaintext, err := key.Decode(response.Body) - common.Timeline("decode done") - pretty.Guard(err == nil, 6, "Decode problem with body %s, reason: %v", response.Body, err) - - common.Log("Response: %s", string(plaintext)) -} - -func init() { - internalCmd.AddCommand(e2eeCmd) - e2eeCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room operations.") - e2eeCmd.Flags().IntVarP(&encryptionVersion, "use", "u", 1, "Which version of encryption method to test (1 or 2)") - e2eeCmd.Flags().StringVarP(&workspaceId, "workspace", "", "", "Workspace id to get assistant information.") - e2eeCmd.Flags().StringVarP(&assistantId, "assistant", "", "", "Assistant id to execute.") -} diff --git a/common/version.go b/common/version.go index b80fed4c..390843ba 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.12.1` + Version = `v17.13.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 96bc0343..99383e2e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.13.0 (date: 24.1.2024) + +- removing internal ECC experiment code (since it never get proper support) +- this should also remove one security vulnerability (Terrapin) hopefully + ## v17.12.1 (date: 27.11.2023) - bugfix: removing duplicates and existing holotree from PATHs before adding diff --git a/go.mod b/go.mod index c2fef479..438cd34b 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,14 @@ require ( github.com/spf13/viper v1.17.0 golang.org/x/sys v0.13.0 golang.org/x/term v0.13.0 - gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 ) +exclude ( + golang.org/x/crypto v0.0.0 + golang.org/x/crypto v0.13.0 +) + require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -31,7 +35,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 1ffb6719..63384bfa 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -494,8 +492,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/operations/assistant.go b/operations/assistant.go index 47d0155e..171ad9ac 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -294,18 +294,13 @@ func StopAssistantRun(client cloud.Client, account *account, workspaceId, assist return nil } -func StartAssistantRun(client cloud.Client, account *account, workspaceId, assistantId string, ecc bool) (*AssistantRobot, error) { +func StartAssistantRun(client cloud.Client, account *account, workspaceId, assistantId string) (*AssistantRobot, error) { common.Timeline("start assistant run: %q", assistantId) credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { return nil, err } - var key Ephemeral - if ecc { - key, err = GenerateEphemeralEccKey() - } else { - key, err = GenerateEphemeralKey() - } + key, err := GenerateEphemeralKey() if err != nil { return nil, err } diff --git a/operations/encryptionv2.go b/operations/encryptionv2.go deleted file mode 100644 index 01630eb8..00000000 --- a/operations/encryptionv2.go +++ /dev/null @@ -1,78 +0,0 @@ -package operations - -import ( - "bytes" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "encoding/json" - "encoding/pem" - "io" - - "github.com/robocorp/rcc/common" - "gopkg.in/square/go-jose.v2" -) - -type EncryptionV2 struct { - *ecdsa.PrivateKey -} - -func GenerateEphemeralEccKey() (Ephemeral, error) { - common.Timeline("start ephemeral key generation") - defer common.Timeline("done ephemeral key generation") - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return nil, err - } - return &EncryptionV2{key}, nil -} - -func (it *EncryptionV2) PublicPEM() (string, error) { - bytes, err := x509.MarshalPKIXPublicKey(it.Public()) - if err != nil { - return "", err - } - block := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: []byte(bytes), - } - result := pem.EncodeToMemory(block) - return string(result), nil -} - -func (it *EncryptionV2) RequestObject(payload interface{}) ([]byte, error) { - result := make(Token) - encryption := make(Token) - encryption["scheme"] = "rc-encryption-v2" - envelope, err := it.PublicPEM() - if err != nil { - return nil, err - } - encryption["publicKey"] = envelope - result["encryption"] = encryption - if payload != nil { - result["payload"] = payload - } - return json.Marshal(result) -} - -func (it *EncryptionV2) RequestBody(payload interface{}) (io.Reader, error) { - blob, err := it.RequestObject(payload) - if err != nil { - return nil, err - } - return bytes.NewReader(blob), nil -} - -func (it *EncryptionV2) Decode(blob []byte) ([]byte, error) { - jwe, err := jose.ParseEncrypted(string(blob)) - if err != nil { - return nil, err - } - payload, err := jwe.Decrypt(it.PrivateKey) - if err != nil { - return nil, err - } - return payload, nil -} diff --git a/operations/security_test.go b/operations/security_test.go index 91c6c3bd..1b5be482 100644 --- a/operations/security_test.go +++ b/operations/security_test.go @@ -1,9 +1,7 @@ package operations_test import ( - "crypto/ecdsa" "crypto/rsa" - "fmt" "strings" "testing" @@ -11,35 +9,6 @@ import ( "github.com/robocorp/rcc/operations" ) -func TestCanCreatePrivateEccKey(t *testing.T) { - must, wont := hamlet.Specifications(t) - - ephemeral, err := operations.GenerateEphemeralEccKey() - must.Nil(err) - wont.Nil(ephemeral) - key, ok := ephemeral.(*operations.EncryptionV2) - must.True(ok) - wont.Nil(key) - wont.Nil(key.Public()) - publicKey, ok := key.Public().(*ecdsa.PublicKey) - must.True(ok) - wont.Nil(publicKey) - envelope, err := key.PublicPEM() - fmt.Println(envelope) - must.Nil(err) - must.Equal(215, len(envelope)) - body, err := key.RequestObject(nil) - must.Nil(err) - must.Equal(279, len(body)) - textual := string(body) - must.True(strings.Contains(textual, "encryption")) - must.True(strings.Contains(textual, "scheme")) - must.True(strings.Contains(textual, "publicKey")) - reader, err := key.RequestBody("hello, world!") - must.Nil(err) - wont.Nil(reader) -} - func TestCanCreatePrivateRsaKey(t *testing.T) { must, wont := hamlet.Specifications(t) From dbd464af4049c60e8d451926a92f7029857daff6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 2 Feb 2024 10:40:03 +0200 Subject: [PATCH 469/516] Add venv support (v17.14.0) - new experimental command `rcc holotree venv` to support python virtual environments; this is still "work in progress" --- cmd/holotreeVenv.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 cmd/holotreeVenv.go diff --git a/cmd/holotreeVenv.go b/cmd/holotreeVenv.go new file mode 100644 index 00000000..f0575a06 --- /dev/null +++ b/cmd/holotreeVenv.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/journal" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/shell" + + "github.com/spf13/cobra" +) + +func deleteByExactIdentity(exact string) { + _, roots := htfs.LoadCatalogs() + for _, label := range roots.FindEnvironments([]string{exact}) { + common.Log("Removing %v", label) + err := roots.RemoveHolotreeSpace(label) + pretty.Guard(err == nil, 4, "Error: %v", err) + } +} + +var holotreeVenvCmd = &cobra.Command{ + Use: "venv conda.yaml+", + Short: "Create user managed virtual python environment inside automation folder.", + Long: "Create user managed virtual python environment inside automation folder.", + Args: cobra.MinimumNArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + defer journal.BuildEventStats("venv") + if common.DebugFlag() { + defer common.Stopwatch("Holotree venv command lasted").Report() + } + + // following settings are forced in venv environments + common.UnmanagedSpace = true + common.ExternallyManaged = true + common.ControllerType = "venv" + + where, err := os.Getwd() + pretty.Guard(err == nil, 1, "Error: %v", err) + location := filepath.Join(where, "venv") + + previous := pathlib.IsDir(location) + if holotreeForce && previous { + pretty.Note("Trying to remove existing venv at %q ...", location) + err := pathlib.TryRemoveAll("venv", location) + pretty.Guard(err == nil, 2, "Error: %v", err) + } + + pretty.Guard(!pathlib.Exists(location), 3, "Name %q aready exists! Remove it, or use force.", location) + + if holotreeForce { + identity := htfs.ControllerSpaceName([]byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + deleteByExactIdentity(identity) + } + + env := holotreeExpandEnvironment(args, "", "", "", 0, holotreeForce) + pretty.Note("Trying to make new venv at %q ...", location) + task := shell.New(env, ".", "python", "-m", "venv", "--copies", location) + code, err := task.Execute(false) + pretty.Guard(err == nil, 5, "Error: %v", err) + pretty.Guard(code == 0, 6, "Exit code %d from venv creation.", code) + + pretty.Highlight("New venv is located at %q. Use activation use venv/bin/activate scripts.", location) + + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeVenvCmd) + rootCmd.AddCommand(holotreeVenvCmd) + + holotreeVenvCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") + holotreeVenvCmd.Flags().BoolVarP(&holotreeForce, "force", "f", false, "Force environment creation by deleting unmanaged space. Dangerous, do not use unless you understand what it means.") +} diff --git a/common/version.go b/common/version.go index 390843ba..cf18e9de 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.13.0` + Version = `v17.14.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 99383e2e..10f02913 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.14.0 (date: 2.2.2024) + +- new experimental command `rcc holotree venv` to support python virtual + environments; this is still "work in progress" + ## v17.13.0 (date: 24.1.2024) - removing internal ECC experiment code (since it never get proper support) From 880842481eadf7480dcd0359590578f5d352a9f5 Mon Sep 17 00:00:00 2001 From: Solomon Jacobs Date: Tue, 16 Jan 2024 17:21:53 +0100 Subject: [PATCH 470/516] Avoid error upon missing `BUILTIN\Users` In German Windows installations there is no group with the name `BUILTIN\Users`. In such cases the command ``` rcc holotree shared --enable ``` causes the error ``` exit status 1332, command: icacls [C:/ProgramData/robocorp /grant BUILTIN\Users*:(OI)(CI)M /T /Q] ``` Replacing the name form of the group by the corresponding SID resolves the issue. The SID `S-1-5-32-545` maps to `BUILTIN\Users` as follows: * A revision level (1) * An identifier authority value (5, NT Authority) * A domain identifier (32, Builtin) * A relative identifier (545, Users) --- cmd/command_windows.go | 2 +- docs/recipes.md | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/cmd/command_windows.go b/cmd/command_windows.go index 6a378ee5..4b2f3d70 100644 --- a/cmd/command_windows.go +++ b/cmd/command_windows.go @@ -18,7 +18,7 @@ func osSpecificHolotreeSharing(enable bool) { parent := filepath.Dir(common.HoloLocation()) _, err := pathlib.ForceSharedDir(parent) pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) - task := shell.New(nil, ".", "icacls", "C:/ProgramData/robocorp", "/grant", "BUILTIN\\Users:(OI)(CI)M", "/T", "/Q") + task := shell.New(nil, ".", "icacls", "C:/ProgramData/robocorp", "/grant", "*S-1-5-32-545:(OI)(CI)M", "/T", "/Q") _, err = task.Execute(false) pretty.Guard(err == nil, 2, "Could not set 'icacls' settings, reason: %v", err) err = os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) diff --git a/docs/recipes.md b/docs/recipes.md index 8570f0cb..322bf15d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -420,14 +420,6 @@ The commands to enable the shared locations are: * Linux: `sudo rcc holotree shared --enable` * Shared location: `/opt/robocorp` -Note: On Windows the command below assumes the standard `BUILTIN\Users` -user group is present. -If your organization has replaced this you can grant the permission with: - -``` -icacls "C:\ProgramData\robocorp" /grant "BUILTIN\Users":(OI)(CI)M /T -``` - To switch the user to using shared holotrees use the following command. ```sh From 27588de2d367c8967b61bab519a48573206fc06c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 2 Feb 2024 11:25:21 +0200 Subject: [PATCH 471/516] Pull request for Windows icacl (v17.15.0) --- common/version.go | 2 +- docs/changelog.md | 7 +++++++ docs/recipes.md | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index cf18e9de..f3690647 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.14.0` + Version = `v17.15.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 10f02913..5ba6f8a5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.15.0 (date: 2.2.2024) + +- pull request from https://github.com/SoloJacobs/rcc relating to Windows + icacls usage. Thank you, Solomon Jacobs and Simon Meggle for bringing + this up. +- this closes #54 + ## v17.14.0 (date: 2.2.2024) - new experimental command `rcc holotree venv` to support python virtual diff --git a/docs/recipes.md b/docs/recipes.md index 322bf15d..f63b47b2 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -420,6 +420,14 @@ The commands to enable the shared locations are: * Linux: `sudo rcc holotree shared --enable` * Shared location: `/opt/robocorp` +Note: On Windows the command below assumes the standard `BUILTIN\Users` +user group is present. +If your organization has replaced this you can grant the permission with: + +``` +icacls "C:\ProgramData\robocorp" /grant "*S-1-5-32-545:(OI)(CI)M" /T +``` + To switch the user to using shared holotrees use the following command. ```sh From 3e32a86318a5e5d2a8a114f70e1a34a8a86d7b3e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 5 Feb 2024 11:58:31 +0200 Subject: [PATCH 472/516] Bugfix: venv, paths and options (v17.15.1) - bugfix: venv creation was missing `--system-site-packages` option, added - bugfix: in venv creation, picking path from actual environment and then using python there to create venv --- cmd/holotreeVenv.go | 17 ++++++++++++----- common/version.go | 2 +- docs/changelog.md | 6 ++++++ pathlib/targetpath.go | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cmd/holotreeVenv.go b/cmd/holotreeVenv.go index f0575a06..a535fd8b 100644 --- a/cmd/holotreeVenv.go +++ b/cmd/holotreeVenv.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" @@ -59,13 +60,19 @@ var holotreeVenvCmd = &cobra.Command{ } env := holotreeExpandEnvironment(args, "", "", "", 0, holotreeForce) - pretty.Note("Trying to make new venv at %q ...", location) - task := shell.New(env, ".", "python", "-m", "venv", "--copies", location) + envPath := pathlib.EnvironmentPath(env) + python, ok := envPath.Which("python", conda.FileExtensions) + if !ok { + python, ok = envPath.Which("python3", conda.FileExtensions) + } + pretty.Guard(ok, 5, "For some reason, could not find python executable in environment paths. Report a bug. PATH: %q", envPath) + pretty.Note("Trying to make new venv at %q using %q ...", location, python) + task := shell.New(env, ".", python, "-m", "venv", "--system-site-packages", location) code, err := task.Execute(false) - pretty.Guard(err == nil, 5, "Error: %v", err) - pretty.Guard(code == 0, 6, "Exit code %d from venv creation.", code) + pretty.Guard(err == nil, 6, "Error: %v", err) + pretty.Guard(code == 0, 7, "Exit code %d from venv creation.", code) - pretty.Highlight("New venv is located at %q. Use activation use venv/bin/activate scripts.", location) + pretty.Highlight("New venv is located at %q. For activation, use venv/bin/activate scripts.", location) pretty.Ok() }, diff --git a/common/version.go b/common/version.go index f3690647..8228f994 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.15.0` + Version = `v17.15.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5ba6f8a5..9b0ad037 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.15.1 (date: 5.2.2024) + +- bugfix: venv creation was missing `--system-site-packages` option, added +- bugfix: in venv creation, picking path from actual environment and then + using python there to create venv + ## v17.15.0 (date: 2.2.2024) - pull request from https://github.com/SoloJacobs/rcc relating to Windows diff --git a/pathlib/targetpath.go b/pathlib/targetpath.go index 241c52b5..8ed01729 100644 --- a/pathlib/targetpath.go +++ b/pathlib/targetpath.go @@ -44,6 +44,16 @@ func TargetPath() PathParts { return noPreviousHolotrees(noDuplicates(filepath.SplitList(os.Getenv("PATH")))) } +func EnvironmentPath(environment []string) PathParts { + path := "" + for _, entry := range environment { + if strings.HasPrefix(strings.ToLower(entry), "path=") { + path = entry[5:] + } + } + return noDuplicates(filepath.SplitList(path)) +} + func PathFrom(parts ...string) PathParts { if parts == nil { return PathParts{} From 66e272af97a61ae82f284a21705c0b52445eb51e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 5 Feb 2024 14:15:07 +0200 Subject: [PATCH 473/516] Bugfix: activation script locations (v17.15.2) - bugfix: venv activation script search performed after initialization --- cmd/holotreeVenv.go | 18 +++++++++++++++++- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cmd/holotreeVenv.go b/cmd/holotreeVenv.go index a535fd8b..983e8468 100644 --- a/cmd/holotreeVenv.go +++ b/cmd/holotreeVenv.go @@ -1,8 +1,10 @@ package cmd import ( + "io/fs" "os" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" @@ -72,12 +74,26 @@ var holotreeVenvCmd = &cobra.Command{ pretty.Guard(err == nil, 6, "Error: %v", err) pretty.Guard(code == 0, 7, "Exit code %d from venv creation.", code) - pretty.Highlight("New venv is located at %q. For activation, use venv/bin/activate scripts.", location) + listActivationScripts(location) pretty.Ok() }, } +func listActivationScripts(root string) { + pretty.Highlight("New venv is located at %s. Following scripts seem to be available:", root) + base := filepath.Dir(root) + filepath.Walk(root, func(path string, entry fs.FileInfo, err error) error { + if entry.Mode().IsRegular() && strings.HasPrefix(strings.ToLower(entry.Name()), "activ") { + short, err := filepath.Rel(base, path) + if err == nil { + pretty.Highlight(" - %s", short) + } + } + return nil + }) +} + func init() { holotreeCmd.AddCommand(holotreeVenvCmd) rootCmd.AddCommand(holotreeVenvCmd) diff --git a/common/version.go b/common/version.go index 8228f994..e80e1d61 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.15.1` + Version = `v17.15.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9b0ad037..7e16c2b2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.15.2 (date: 5.2.2024) + +- bugfix: venv activation script search performed after initialization + ## v17.15.1 (date: 5.2.2024) - bugfix: venv creation was missing `--system-site-packages` option, added From e423500a4a5aa6c1814c76540d8f44b2b92ea288 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Feb 2024 11:41:36 +0200 Subject: [PATCH 474/516] Feature: NO_PROXY configuration support (v17.16.0) - new `NO_PROXY` configuration addition to `settings.yaml` file. - that `NO_PROXY` will override previous OS level configuration, so be careful - this closes #57 --- assets/settings.yaml | 1 + common/version.go | 2 +- conda/robocorp.go | 2 ++ docs/changelog.md | 6 ++++++ operations/diagnostics.go | 1 + robot_tests/profile_beta.yaml | 1 + robot_tests/profiles.robot | 2 ++ settings/api.go | 1 + settings/data.go | 4 ++++ settings/settings.go | 4 ++++ settings/settings_test.go | 1 + 11 files changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index c52a35b5..e006aac7 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -31,6 +31,7 @@ options: no-build: false network: + no-proxy: # no no proxy by default https-proxy: # no proxy by default http-proxy: # no proxy by default diff --git a/common/version.go b/common/version.go index e80e1d61..2bac990f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.15.2` + Version = `v17.16.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 35ccdbd0..e7085e7f 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -120,6 +120,8 @@ func injectNetworkEnvironment(environment []string) []string { environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) environment = appendIfValue(environment, "HTTP_PROXY", settings.Global.HttpProxy()) + environment = appendIfValue(environment, "no_proxy", settings.Global.NoProxy()) + environment = appendIfValue(environment, "NO_PROXY", settings.Global.NoProxy()) if common.WarrantyVoided() { environment = append(environment, "RCC_WARRANTY_VOIDED=true") } diff --git a/docs/changelog.md b/docs/changelog.md index 7e16c2b2..53e70190 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.16.0 (date: 7.2.2024) + +- new `NO_PROXY` configuration addition to `settings.yaml` file. +- that `NO_PROXY` will override previous OS level configuration, so be careful +- this closes #57 + ## v17.15.2 (date: 5.2.2024) - bugfix: venv activation script search performed after initialization diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 1ce7a869..380a61aa 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -91,6 +91,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["config-active-profile"] = settings.Global.Name() result.Details["config-https-proxy"] = settings.Global.HttpsProxy() result.Details["config-http-proxy"] = settings.Global.HttpProxy() + result.Details["config-no-proxy"] = settings.Global.NoProxy() result.Details["config-ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) result.Details["config-ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) result.Details["config-legacy-renegotiation-allowed"] = fmt.Sprintf("%v", settings.Global.LegacyRenegotiation()) diff --git a/robot_tests/profile_beta.yaml b/robot_tests/profile_beta.yaml index e2910aa5..a9bf2c38 100644 --- a/robot_tests/profile_beta.yaml +++ b/robot_tests/profile_beta.yaml @@ -6,6 +6,7 @@ settings: ssl-no-revoke: true legacy-renegotiation-allowed: true network: + no-proxy: noproxy.betaputkinen.net https-proxy: http://bad.betaputkinen.net:1234/ http-proxy: http://bad.betaputkinen.net:2345/ meta: diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index 44cc7085..87a0d8b8 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -56,6 +56,7 @@ Goal: Quick diagnostics can show alpha profile information Must Have "config-settings-yaml-used": "true" Must Have "config-ssl-no-revoke": "false" Must Have "config-ssl-verify": "true" + Must Have "config-no-proxy": "" Must Have "config-https-proxy": "" Must Have "config-http-proxy": "" @@ -79,6 +80,7 @@ Goal: Quick diagnostics can show beta profile information Must Have "config-ssl-no-revoke": "true" Must Have "config-ssl-verify": "false" Must Have "config-legacy-renegotiation-allowed": "true" + Must Have "config-no-proxy": "noproxy.betaputkinen.net" Must Have "config-https-proxy": "http://bad.betaputkinen.net:1234/" Must Have "config-http-proxy": "http://bad.betaputkinen.net:2345/" diff --git a/settings/api.go b/settings/api.go index 73145532..1d0914ed 100644 --- a/settings/api.go +++ b/settings/api.go @@ -27,6 +27,7 @@ type Api interface { CondaLink(page string) string Hostnames() []string ConfiguredHttpTransport() *http.Transport + NoProxy() string HttpsProxy() string HttpProxy() string HasPipRc() bool diff --git a/settings/data.go b/settings/data.go index ba232557..4374e4ca 100644 --- a/settings/data.go +++ b/settings/data.go @@ -294,6 +294,7 @@ func (it *Meta) onTopOf(target *Settings) { } type Network struct { + NoProxy string `yaml:"no-proxy" json:"no-proxy"` HttpsProxy string `yaml:"https-proxy" json:"https-proxy"` HttpProxy string `yaml:"http-proxy" json:"http-proxy"` } @@ -302,6 +303,9 @@ func (it *Network) onTopOf(target *Settings) { if target.Network == nil { target.Network = &Network{} } + if len(it.NoProxy) > 0 { + target.Network.NoProxy = it.NoProxy + } if len(it.HttpsProxy) > 0 { target.Network.HttpsProxy = it.HttpsProxy } diff --git a/settings/settings.go b/settings/settings.go index 145bf735..5f7ea1d4 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -175,6 +175,10 @@ func (it gateway) CondaURL() string { return it.Endpoint("conda") } +func (it gateway) NoProxy() string { + return it.settings().Network.NoProxy +} + func (it gateway) HttpsProxy() string { return it.settings().Network.HttpsProxy } diff --git a/settings/settings_test.go b/settings/settings_test.go index f1cd322d..b2da0ed1 100644 --- a/settings/settings_test.go +++ b/settings/settings_test.go @@ -35,5 +35,6 @@ func TestThatSomeDefaultValuesAreVisible(t *testing.T) { must_be.Equal("", settings.Global.CondaURL()) must_be.Equal("", settings.Global.HttpProxy()) must_be.Equal("", settings.Global.HttpsProxy()) + must_be.Equal("", settings.Global.NoProxy()) must_be.Equal(9, len(settings.Global.Hostnames())) } From f85b2b0a12a11995be619b3b543cc8de297f5089 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 9 Feb 2024 13:36:33 +0200 Subject: [PATCH 475/516] Feature: added depxtraction.py to created venv (v17.17.0) - adding depxtraction as part of `rcc holotree venv` creation --- Rakefile | 2 ++ assets/depxtraction.py | 66 +++++++++++++++++++++++++++++++++++++++++ blobs/assets/.gitignore | 1 + blobs/embedded.go | 1 + cmd/holotreeVenv.go | 17 +++++++++-- common/version.go | 2 +- docs/changelog.md | 4 +++ 7 files changed, 90 insertions(+), 3 deletions(-) create mode 100755 assets/depxtraction.py diff --git a/Rakefile b/Rakefile index e1428d7b..e38329b1 100644 --- a/Rakefile +++ b/Rakefile @@ -24,6 +24,7 @@ task :noassets do rm_f FileList['blobs/assets/micromamba.*'] rm_f FileList['blobs/assets/*.zip'] rm_f FileList['blobs/assets/*.yaml'] + rm_f FileList['blobs/assets/*.py'] rm_f FileList['blobs/assets/man/*.txt'] rm_f FileList['blobs/docs/*.md'] end @@ -54,6 +55,7 @@ task :assets => [:noassets, :micromamba] do end cp FileList['assets/*.txt'], 'blobs/assets/' cp FileList['assets/*.yaml'], 'blobs/assets/' + cp FileList['assets/*.py'], 'blobs/assets/' cp FileList['assets/man/*.txt'], 'blobs/assets/man/' cp FileList['docs/*.md'], 'blobs/docs/' end diff --git a/assets/depxtraction.py b/assets/depxtraction.py new file mode 100755 index 00000000..c3fee3f4 --- /dev/null +++ b/assets/depxtraction.py @@ -0,0 +1,66 @@ +#!/bin/env python3 + +import pip, re, sys + +from collections import namedtuple +from importlib import metadata + +REJECTED = {'pkg_resources', 'pkgutil_resolve_name', 'pip', 'setuptools', 'wheel'} + +NAMEFORM = re.compile(r'^([a-z0-9](?:[a-z0-9._-]*?[a-z0-9])?)([^a-z0-9._-].*)?$', re.I) +EXTRAFORM = re.compile(r'\bextra\s*=') + +Metadata = namedtuple('Metadata', 'key name version needs') + +def normalize(name): + return re.sub(r'[-_.]+', '-', name).lower() + +def unify(name): + return normalize(str(name).strip()) + +def list_modules(): + for candidate in metadata.distributions(): + yield candidate + +def parselet(text): + head, *rest = map(str.strip, text.split(';')) + name, *ignore = map(str.strip, filter(bool, NAMEFORM.match(head).groups())) + extra = any(map(EXTRAFORM.match, rest)) + return extra, unify(name) + +def conda_yaml(resolved): + print('channels:\n- conda-forge\ndependencies:') + python = sys.version_info + print(f'- python={python.major}.{python.minor}.{python.micro}') + print(f'- pip={pip.__version__}') + if version := resolved.pop('robocorp-truststore', None): + print(f'- robocorp-truststore={version}') + if resolved: + print(f'- pip:') + for name, version in resolved.items(): + print(f' - {name}=={version}') + +def process(): + metadata = dict() + for module in list_modules(): + name = module.metadata.get('name') + key = unify(name) + metadata[key] = Metadata(key, name, module.version, module.requires or tuple()) + + cyclic = set() + toplevel = set(metadata.keys()) + tuple(map(toplevel.discard, REJECTED)) + for package, needs in sorted(metadata.items()): + for entry in needs.needs: + rejected, name = parselet(entry) + if (package, name) in cyclic: + continue + if not rejected: + cyclic.add((name, package)) + toplevel.discard(name) + + resolved = dict([metadata[x].name, metadata[x].version] for x in sorted(toplevel)) + conda_yaml(resolved) + +if __name__ == '__main__': + process() diff --git a/blobs/assets/.gitignore b/blobs/assets/.gitignore index 68e6ed7b..3b54ee8b 100644 --- a/blobs/assets/.gitignore +++ b/blobs/assets/.gitignore @@ -1,4 +1,5 @@ *.zip *.yaml *.txt +*.py micromamba* diff --git a/blobs/embedded.go b/blobs/embedded.go index 6b5bcce1..e1097385 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -14,6 +14,7 @@ const ( //go:embed assets/*.yaml docs/*.md //go:embed assets/*.zip assets/man/*.txt //go:embed assets/*.txt +//go:embed assets/*.py var content embed.FS func Asset(name string) ([]byte, error) { diff --git a/cmd/holotreeVenv.go b/cmd/holotreeVenv.go index 983e8468..ce3e0a88 100644 --- a/cmd/holotreeVenv.go +++ b/cmd/holotreeVenv.go @@ -1,13 +1,16 @@ package cmd import ( + "fmt" "io/fs" "os" "path/filepath" "strings" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" @@ -74,24 +77,34 @@ var holotreeVenvCmd = &cobra.Command{ pretty.Guard(err == nil, 6, "Error: %v", err) pretty.Guard(code == 0, 7, "Exit code %d from venv creation.", code) - listActivationScripts(location) + target := listActivationScripts(location) + if len(target) > 0 { + blob, err := blobs.Asset("assets/depxtraction.py") + fail.Fast(err) + location := filepath.Join(target, "depxtraction.py") + fail.Fast(os.WriteFile(location, blob, 0o755)) + fmt.Printf("Experimental dependency extraction tool is available at %q.\nTry it after pip installing things into your venv.\n", location) + } pretty.Ok() }, } -func listActivationScripts(root string) { +func listActivationScripts(root string) string { pretty.Highlight("New venv is located at %s. Following scripts seem to be available:", root) base := filepath.Dir(root) + pathcandidate := "" filepath.Walk(root, func(path string, entry fs.FileInfo, err error) error { if entry.Mode().IsRegular() && strings.HasPrefix(strings.ToLower(entry.Name()), "activ") { short, err := filepath.Rel(base, path) if err == nil { pretty.Highlight(" - %s", short) } + pathcandidate = filepath.Dir(short) } return nil }) + return pathcandidate } func init() { diff --git a/common/version.go b/common/version.go index 2bac990f..11ac3e07 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.16.0` + Version = `v17.17.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 53e70190..3249cf66 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.17.0 (date: 9.2.2024) + +- adding depxtraction as part of `rcc holotree venv` creation + ## v17.16.0 (date: 7.2.2024) - new `NO_PROXY` configuration addition to `settings.yaml` file. From b18cdacf4cd9dbc1c921b2c0acc1ab9e7d02dae9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 12 Feb 2024 08:53:40 +0200 Subject: [PATCH 476/516] Bugfix: .use file pollution (v17.17.1) - fixed space .use file to be written only when path is actually known --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 11ac3e07..04e6d032 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.17.0` + Version = `v17.17.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3249cf66..e6f126e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.17.1 (date: 12.2.2024) + +- fixed space .use file to be written only when path is actually known + ## v17.17.0 (date: 9.2.2024) - adding depxtraction as part of `rcc holotree venv` creation diff --git a/htfs/commands.go b/htfs/commands.go index a3aa3840..2824dd78 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -54,8 +54,10 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal pretty.Regression(15, "Holotree restoration failure, see above [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) } else { pretty.Progress(15, "Fresh %sholotree done [with %d workers on %d CPUs].", externally, anywork.Scale(), runtime.NumCPU()) - usefile := fmt.Sprintf("%s.use", path) - pathlib.AppendFile(usefile, []byte{'.'}) + if len(path) > 0 { + usefile := fmt.Sprintf("%s.use", path) + pathlib.AppendFile(usefile, []byte{'.'}) + } } if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) From 48d49515b0e98919306779d267733c79853781c4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 14 Feb 2024 12:10:25 +0200 Subject: [PATCH 477/516] Refactoring: depxtraction details (v17.17.2) - depxtraction output update and refactoring code --- assets/depxtraction.py | 47 ++++++++++++++++++++++++++++++------------ common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/assets/depxtraction.py b/assets/depxtraction.py index c3fee3f4..6c644737 100755 --- a/assets/depxtraction.py +++ b/assets/depxtraction.py @@ -5,18 +5,37 @@ from collections import namedtuple from importlib import metadata -REJECTED = {'pkg_resources', 'pkgutil_resolve_name', 'pip', 'setuptools', 'wheel'} +REJECTED = {'pip', 'pkg_resources', 'pkgutil_resolve_name', 'setuptools', 'wheel'} +CONDA_FORGE = {'robocorp-truststore'} +EMPTY = tuple() NAMEFORM = re.compile(r'^([a-z0-9](?:[a-z0-9._-]*?[a-z0-9])?)([^a-z0-9._-].*)?$', re.I) EXTRAFORM = re.compile(r'\bextra\s*=') +NORMALIZE = re.compile(r'[-_.]+') Metadata = namedtuple('Metadata', 'key name version needs') -def normalize(name): - return re.sub(r'[-_.]+', '-', name).lower() +HEADER = f''' +# This was generated by `{__file__}` script. +# This is experimental feature for getting `environment.yaml` from installed base. +# It only supports simple `pip` environments, where most dependencies are coming +# from pypi.org. If your desire is to have addtional packages from `conda-forge` +# those must be maintained manually on above `pip:` section. + +channels: +- conda-forge +dependencies: +'''.strip() -def unify(name): - return normalize(str(name).strip()) +FOOTER = ''' +# NOTE: building above environment might fail, because dependencies are +# collected heuristically. So once you have generated new environment +# configuration, you should actually try to build it using `rcc`. +''' + +def normalize(name): + return NORMALIZE.sub('-', str(name)).lower().strip() + return re.sub(r'[-_.]+', '-', str(name)).lower().strip() def list_modules(): for candidate in metadata.distributions(): @@ -26,26 +45,28 @@ def parselet(text): head, *rest = map(str.strip, text.split(';')) name, *ignore = map(str.strip, filter(bool, NAMEFORM.match(head).groups())) extra = any(map(EXTRAFORM.match, rest)) - return extra, unify(name) + return extra, normalize(name) -def conda_yaml(resolved): - print('channels:\n- conda-forge\ndependencies:') +def environment_yaml(resolved): + print(HEADER) python = sys.version_info print(f'- python={python.major}.{python.minor}.{python.micro}') print(f'- pip={pip.__version__}') - if version := resolved.pop('robocorp-truststore', None): - print(f'- robocorp-truststore={version}') + for conda in CONDA_FORGE: + if version := resolved.pop(conda, None): + print(f'- {conda}={version}') if resolved: print(f'- pip:') for name, version in resolved.items(): print(f' - {name}=={version}') + print(FOOTER) def process(): metadata = dict() for module in list_modules(): name = module.metadata.get('name') - key = unify(name) - metadata[key] = Metadata(key, name, module.version, module.requires or tuple()) + key = normalize(name) + metadata[key] = Metadata(key, name, module.version, module.requires or EMPTY) cyclic = set() toplevel = set(metadata.keys()) @@ -60,7 +81,7 @@ def process(): toplevel.discard(name) resolved = dict([metadata[x].name, metadata[x].version] for x in sorted(toplevel)) - conda_yaml(resolved) + environment_yaml(resolved) if __name__ == '__main__': process() diff --git a/common/version.go b/common/version.go index 04e6d032..1e21d880 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.17.1` + Version = `v17.17.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index e6f126e1..0d872901 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.17.2 (date: 14.2.2024) + +- depxtraction output update and refactoring code + ## v17.17.1 (date: 12.2.2024) - fixed space .use file to be written only when path is actually known From 7e2cffbb13f6887bf43c5271ed595ef94a17cdd3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Sat, 17 Feb 2024 09:17:03 +0200 Subject: [PATCH 478/516] Bugfix: option to hide unwanted log outputs (v17.17.3) - bugfix: unwanted logging output can now be hidden using global option `--log-hide ` (and can be given multiple times) --- cmd/root.go | 1 + common/logger.go | 28 +++++++++++++++++++++++----- common/variables.go | 1 + common/version.go | 2 +- docs/changelog.md | 5 +++++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index b78c5d0c..4fb2989a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -128,6 +128,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", false, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.NoTempManagement, "no-temp-management", "", false, "rcc wont do any temp directory management ... DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.NoPycManagement, "no-pyc-management", "", false, "rcc wont do any .pyc file management ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().StringArrayVarP(&common.LogHides, "log-hide", "", []string{}, "hide logging output that matches given text fragment and this option can be given multiple times") } func initConfig() { diff --git a/common/logger.go b/common/logger.go index 31ded87b..20739b33 100644 --- a/common/logger.go +++ b/common/logger.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "runtime" + "strings" "sync" "time" ) @@ -44,10 +45,21 @@ func init() { go loggerLoop(logsource) } +func AcceptableOutput(message string) bool { + for _, fragment := range LogHides { + if strings.Contains(message, fragment) { + return false + } + } + return true +} + func printout(out *os.File, message string) { - logbarrier.Add(1) - logsource <- func() (*os.File, string) { - return out, message + if AcceptableOutput(message) { + logbarrier.Add(1) + logsource <- func() (*os.File, string) { + return out, message + } } } @@ -88,8 +100,14 @@ func Trace(format string, details ...interface{}) error { } func Stdout(format string, details ...interface{}) { - fmt.Fprintf(os.Stdout, format, details...) - os.Stdout.Sync() + message := format + if len(details) > 0 { + message = fmt.Sprintf(format, details...) + } + if AcceptableOutput(message) { + fmt.Fprint(os.Stdout, message) + os.Stdout.Sync() + } } func WaitLogs() { diff --git a/common/variables.go b/common/variables.go index b3921ea0..8a4ddea1 100644 --- a/common/variables.go +++ b/common/variables.go @@ -63,6 +63,7 @@ var ( Clock *stopwatch randomIdentifier string verbosity Verbosity + LogHides []string ) func init() { diff --git a/common/version.go b/common/version.go index 1e21d880..701e4fce 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.17.2` + Version = `v17.17.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0d872901..3cd7c90b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.17.3 (date: 17.2.2024) + +- bugfix: unwanted logging output can now be hidden using global option + `--log-hide ` (and can be given multiple times) + ## v17.17.2 (date: 14.2.2024) - depxtraction output update and refactoring code From 9e67dcdc786ec1849855cf8b397544b0ba9ee34c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 19 Feb 2024 11:43:11 +0200 Subject: [PATCH 479/516] Docs: venv and depxtraction docs (v17.17.4) - added `venv.md` for start of documentation for `rcc venv` and depxtraction tooling and ideas how to use them - added new man page command into rcc: `rcc man venv` --- cmd/man.go | 7 ++++ common/version.go | 2 +- docs/README.md | 94 +++++++++++++++++++++++++--------------------- docs/changelog.md | 6 +++ docs/venv.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++ scripts/toc.py | 1 + 6 files changed, 161 insertions(+), 44 deletions(-) create mode 100644 docs/venv.md diff --git a/cmd/man.go b/cmd/man.go index 3b88d16d..06eef1d6 100644 --- a/cmd/man.go +++ b/cmd/man.go @@ -86,6 +86,13 @@ func init() { Run: makeShowDoc("tutorial", "assets/man/tutorial.txt"), } + manCmd.AddCommand(&cobra.Command{ + Use: "venv", + Short: "Show virtual environment documentation.", + Long: "Show virtual environment documentation.", + Run: makeShowDoc("venv", "docs/venv.md"), + }) + manCmd.AddCommand(&cobra.Command{ Use: "vocabulary", Short: "Show vocabulary documentation", diff --git a/common/version.go b/common/version.go index 701e4fce..b121d5b0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.17.3` + Version = `v17.17.4` ) diff --git a/docs/README.md b/docs/README.md index 7793eb30..2e5cd926 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,46 +95,54 @@ ### 5.4 [Deleting catalogs and spaces](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#deleting-catalogs-and-spaces) ### 5.5 [Keeping hololib consistent](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#keeping-hololib-consistent) ### 5.6 [Summary of maintenance related commands](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#summary-of-maintenance-related-commands) -## 6 [Troubleshooting guidelines and known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#troubleshooting-guidelines-and-known-solutions) -### 6.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) -### 6.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) -### 6.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) -### 6.4 [Network access related troubleshooting questions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#network-access-related-troubleshooting-questions) -### 6.5 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) -#### 6.5.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) -#### 6.5.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) -## 7 [Vocabulary](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#vocabulary) -### 7.1 [Blueprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#blueprint) -### 7.2 [Catalog](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#catalog) -### 7.3 [Controller](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#controller) -### 7.4 [Diagnostics](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#diagnostics) -### 7.5 [Dirty environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#dirty-environment) -### 7.6 [Environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#environment) -### 7.7 [Fingerprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#fingerprint) -### 7.8 [Holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#holotree) -### 7.9 [Hololib](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#hololib) -### 7.10 [Identity](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#identity) -### 7.11 [Platform](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#platform) -### 7.12 [Prebuild environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#prebuild-environment) -### 7.13 [Pristine environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#pristine-environment) -### 7.14 [Private holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#private-holotree) -### 7.15 [Profile](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#profile) -### 7.16 [Robot](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#robot) -### 7.17 [Shared holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#shared-holotree) -### 7.18 [Space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#space) -### 7.19 [Unmanaged holotree space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#unmanaged-holotree-space) -### 7.20 [User](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#user) -## 8 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) -### 8.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) -### 8.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) -### 8.3 [Version 9.x: between Jan 15, 2021 and Jun 10, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-9x-between-jan-15-2021-and-jun-10-2021) -### 8.4 [Version 8.x: between Jan 4, 2021 and Jan 18, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-8x-between-jan-4-2021-and-jan-18-2021) -### 8.5 [Version 7.x: between Dec 1, 2020 and Jan 4, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-7x-between-dec-1-2020-and-jan-4-2021) -### 8.6 [Version 6.x: between Nov 16, 2020 and Nov 30, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-6x-between-nov-16-2020-and-nov-30-2020) -### 8.7 [Version 5.x: between Nov 4, 2020 and Nov 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-5x-between-nov-4-2020-and-nov-16-2020) -### 8.8 [Version 4.x: between Oct 20, 2020 and Nov 2, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-4x-between-oct-20-2020-and-nov-2-2020) -### 8.9 [Version 3.x: between Oct 15, 2020 and Oct 19, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-3x-between-oct-15-2020-and-oct-19-2020) -### 8.10 [Version 2.x: between Sep 16, 2020 and Oct 14, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-2x-between-sep-16-2020-and-oct-14-2020) -### 8.11 [Version 1.x: between Sep 3, 2020 and Sep 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-1x-between-sep-3-2020-and-sep-16-2020) -### 8.12 [Version 0.x: between April 1, 2020 and Sep 8, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-0x-between-april-1-2020-and-sep-8-2020) -### 8.13 [Birth of "Codename: Conman"](https://github.com/robocorp/rcc/blob/master/docs/history.md#birth-of-codename-conman) \ No newline at end of file +## 6 [Support for virtual environments](https://github.com/robocorp/rcc/blob/master/docs/venv.md#support-for-virtual-environments) +### 6.1 [What does it do?](https://github.com/robocorp/rcc/blob/master/docs/venv.md#what-does-it-do) +### 6.2 [How to get started?](https://github.com/robocorp/rcc/blob/master/docs/venv.md#how-to-get-started) +### 6.3 [Limitations of `rcc venv`:](https://github.com/robocorp/rcc/blob/master/docs/venv.md#limitations-of-rcc-venv) +### 6.4 [Dangers of using `--force` in `rcc venv` context.](https://github.com/robocorp/rcc/blob/master/docs/venv.md#dangers-of-using---force-in-rcc-venv-context) +### 6.5 [What is this `depxtraction.py` thing?](https://github.com/robocorp/rcc/blob/master/docs/venv.md#what-is-this-depxtractionpy-thing) +### 6.6 [Limitations of `depxtraction.py`:](https://github.com/robocorp/rcc/blob/master/docs/venv.md#limitations-of-depxtractionpy) +### 6.7 [Ideas for usage](https://github.com/robocorp/rcc/blob/master/docs/venv.md#ideas-for-usage) +## 7 [Troubleshooting guidelines and known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#troubleshooting-guidelines-and-known-solutions) +### 7.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) +### 7.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) +### 7.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) +### 7.4 [Network access related troubleshooting questions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#network-access-related-troubleshooting-questions) +### 7.5 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) +#### 7.5.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) +#### 7.5.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) +## 8 [Vocabulary](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#vocabulary) +### 8.1 [Blueprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#blueprint) +### 8.2 [Catalog](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#catalog) +### 8.3 [Controller](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#controller) +### 8.4 [Diagnostics](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#diagnostics) +### 8.5 [Dirty environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#dirty-environment) +### 8.6 [Environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#environment) +### 8.7 [Fingerprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#fingerprint) +### 8.8 [Holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#holotree) +### 8.9 [Hololib](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#hololib) +### 8.10 [Identity](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#identity) +### 8.11 [Platform](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#platform) +### 8.12 [Prebuild environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#prebuild-environment) +### 8.13 [Pristine environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#pristine-environment) +### 8.14 [Private holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#private-holotree) +### 8.15 [Profile](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#profile) +### 8.16 [Robot](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#robot) +### 8.17 [Shared holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#shared-holotree) +### 8.18 [Space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#space) +### 8.19 [Unmanaged holotree space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#unmanaged-holotree-space) +### 8.20 [User](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#user) +## 9 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) +### 9.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) +### 9.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) +### 9.3 [Version 9.x: between Jan 15, 2021 and Jun 10, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-9x-between-jan-15-2021-and-jun-10-2021) +### 9.4 [Version 8.x: between Jan 4, 2021 and Jan 18, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-8x-between-jan-4-2021-and-jan-18-2021) +### 9.5 [Version 7.x: between Dec 1, 2020 and Jan 4, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-7x-between-dec-1-2020-and-jan-4-2021) +### 9.6 [Version 6.x: between Nov 16, 2020 and Nov 30, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-6x-between-nov-16-2020-and-nov-30-2020) +### 9.7 [Version 5.x: between Nov 4, 2020 and Nov 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-5x-between-nov-4-2020-and-nov-16-2020) +### 9.8 [Version 4.x: between Oct 20, 2020 and Nov 2, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-4x-between-oct-20-2020-and-nov-2-2020) +### 9.9 [Version 3.x: between Oct 15, 2020 and Oct 19, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-3x-between-oct-15-2020-and-oct-19-2020) +### 9.10 [Version 2.x: between Sep 16, 2020 and Oct 14, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-2x-between-sep-16-2020-and-oct-14-2020) +### 9.11 [Version 1.x: between Sep 3, 2020 and Sep 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-1x-between-sep-3-2020-and-sep-16-2020) +### 9.12 [Version 0.x: between April 1, 2020 and Sep 8, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-0x-between-april-1-2020-and-sep-8-2020) +### 9.13 [Birth of "Codename: Conman"](https://github.com/robocorp/rcc/blob/master/docs/history.md#birth-of-codename-conman) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 3cd7c90b..7ce6ce27 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.17.4 (date: 19.2.2024) + +- added `venv.md` for start of documentation for `rcc venv` and depxtraction + tooling and ideas how to use them +- added new man page command into rcc: `rcc man venv` + ## v17.17.3 (date: 17.2.2024) - bugfix: unwanted logging output can now be hidden using global option diff --git a/docs/venv.md b/docs/venv.md new file mode 100644 index 00000000..5983484f --- /dev/null +++ b/docs/venv.md @@ -0,0 +1,95 @@ +# Support for virtual environments + +There is now experimental feature in rcc to create virtual environments on top +of rcc holotree environment. + +## What does it do? + +When you run command `rcc venv`, that will do following things: +- using given `conda.yaml` file, it will create that base environment +- this new environment will be "unmanaged" holotree space, so once created, rcc + wont touch base environment (unless forced) +- it will also be "externally managed" PEP-668 environment, so no additional + things should be pip installed in that environment +- then on top of that base environment, rcc will try automatically create local + project specific `venv` directory and list available activation commands +- in addition to that, rcc also puts experimental `depxtraction.py` script in + same directory (more on that later in this document) + +## How to get started? + +- first you need rcc version 17.17.0 or later available in your system +- then on some directory, you should have `conda.yaml` for base environment, + something like this: + +``` +channels: +- conda-forge +dependencies: +- python=3.10.12 +- pip=23.2.1 +- robocorp-truststore=0.8.0 +``` + +- then in that directory, run command `rcc venv conda.yaml` +- after that, you should see list of activation commands to use this new venv +- after activation, you can use normal pip commands to populate that venv as + you wish + +## Limitations of `rcc venv`: + +- currently naming and location is fixed, so you cannot change those +- this venv is always build on top of holotree space, so that holotree space + must always be there +- and that space is "unmanaged", so idea is, that once created, it is developer + responsibility to delete or force update it if dependencies change +- also other things installed from conda-forget from underlying holotree space + are hidden and only python environment is visible + +## Dangers of using `--force` in `rcc venv` context. + +- unmanged holotrees are not user specific, so be careful when using `--force` + option to recreate those spaces, and recommendation is to use `--space` and + `--controller` options to limit usage to your intentions +- be aware that `--force` makes three things to happen +- first it is needed if `venv` was already created (so rcc wont overwrite + things in venv, unless you really force it) +- second it is used to tell rcc, that also underlying holotree space should + be recreated (maybe with conflicting dependencies) +- third, it forces also full holotree space installation and updating caches + +## What is this `depxtraction.py` thing? + +It is "dependency extraction", with limitations (see below). + +Idea behing `depxtraction.py` is, that when there is modified environment, +where additional dependencies are installed using tools like pip or poetry, +those dependencies can be extracted by tooling into simple `conda.yaml` +format. + +## Limitations of `depxtraction.py`: + +- no conda dependencies detected, and every dependency that python tooling + reports are expected to be from PyPI (except "hardcoded" python, pip and + robocorp-truststore that are defined as bootstrapping dependencies from + conda-forge) +- if there are deeply recursive dependencies (X depends on Y depends on Z + depends on X) then currently those dependencies will vanish, since "root" + dependency is unclear (if you run these cases, please report those, so + that better functionality can be implemented and tested) +- only top level dependencies are resolved and versioned, and listed as + dependencies; subdependency resolving is left for pip resolver to figure out +- and because individual install commands can create inconsistent environment, + it is possible, that once `conda.yaml` is generated out of such environment, + recreation of such environment might actually fail to resolve correctly and + in those cases, you have to adjust generated `conda.yaml` accordingly + +## Ideas for usage + +- start VS Code from CLI inside activated environment +- create rcc venv, install packages manually inside that environment, + make your automation work, once automation is working so far, extract + dependencies, recreate rcc venv and continue iterating ... +- try to run `depxtraction.py` on your system python setup and see what + comes up there (this can be done by just using `depxtraction.py` with + your system python without activating any virtual environments) diff --git a/scripts/toc.py b/scripts/toc.py index d842b11a..55df6a59 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -22,6 +22,7 @@ 'docs/profile_configuration.md', 'docs/environment-caching.md', 'docs/maintenance.md', + 'docs/venv.md', 'docs/troubleshooting.md', 'docs/vocabulary.md', 'docs/history.md', From 8377b99352c9eb0dc9af95f77f8c409768995269 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 23 Feb 2024 12:04:16 +0200 Subject: [PATCH 480/516] Feature: bundled flag and version check (v17.18.0) - new `--bundled` flag to support cases where rcc is bundled inside other apps - first thing behind "bundled" flag is version check (so when flag is given, rcc will never check possible newer version existence) - bugfix: flag handling defaults on peek initialized flags - typofix: on certificate appending failure message --- cmd/root.go | 7 ++++--- common/variables.go | 16 +++++++++------- common/version.go | 2 +- docs/changelog.md | 8 ++++++++ operations/diagnostics.go | 1 + operations/rccversioncheck.go | 4 ++++ robot_tests/fullrun.robot | 10 ++++++++++ settings/settings.go | 2 +- 8 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 4fb2989a..af889116 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -125,10 +125,11 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") - rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", false, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") - rootCmd.PersistentFlags().BoolVarP(&common.NoTempManagement, "no-temp-management", "", false, "rcc wont do any temp directory management ... DO NOT USE (unless you know what you are doing)") - rootCmd.PersistentFlags().BoolVarP(&common.NoPycManagement, "no-pyc-management", "", false, "rcc wont do any .pyc file management ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", common.WarrantyVoidedFlag, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.NoTempManagement, "no-temp-management", "", common.NoTempManagement, "rcc wont do any temp directory management ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.NoPycManagement, "no-pyc-management", "", common.NoPycManagement, "rcc wont do any .pyc file management ... DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringArrayVarP(&common.LogHides, "log-hide", "", []string{}, "hide logging output that matches given text fragment and this option can be given multiple times") + rootCmd.PersistentFlags().BoolVarP(&common.BundledFlag, "bundled", "", common.BundledFlag, "used to tell rcc, that this is bundled use (do not use, unless you know what you are doing)") } func initConfig() { diff --git a/common/variables.go b/common/variables.go index 8a4ddea1..468267ec 100644 --- a/common/variables.go +++ b/common/variables.go @@ -53,6 +53,7 @@ var ( UnmanagedSpace bool FreshlyBuildEnvironment bool WarrantyVoidedFlag bool + BundledFlag bool StageFolder string ControllerType string HolotreeSpace string @@ -76,21 +77,18 @@ func init() { for _, arg := range os.Args { lowargs = append(lowargs, strings.ToLower(arg)) } - // peek CLI options to pre-initialize "Warranty Voided" indicator + // peek CLI options to pre-initialize "Warranty Voided" and other indicators args := set.Set(lowargs) WarrantyVoidedFlag = set.Member(args, "--warranty-voided") + BundledFlag = set.Member(args, "--bundled") + NoTempManagement = set.Member(args, "--no-temp-management") + NoPycManagement = set.Member(args, "--no-pyc-management") if set.Member(args, "--debug") { verbosity = Debugging } if set.Member(args, "--trace") { verbosity = Tracing } - if set.Member(args, "--no-temp-management") { - NoTempManagement = true - } - if set.Member(args, "--no-pyc-management") { - NoPycManagement = true - } // Note: HololibCatalogLocation, HololibLibraryLocation and HololibUsageLocation // are force created from "htfs" direcotry.go init function @@ -144,6 +142,10 @@ func RobocorpLock() string { return filepath.Join(RobocorpHome(), "robocorp.lck") } +func IsBundled() bool { + return BundledFlag +} + func WarrantyVoided() bool { return WarrantyVoidedFlag } diff --git a/common/version.go b/common/version.go index b121d5b0..d09c56ab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.17.4` + Version = `v17.18.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7ce6ce27..a4ca9f2f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v17.18.0 (date: 23.2.2024) + +- new `--bundled` flag to support cases where rcc is bundled inside other apps +- first thing behind "bundled" flag is version check (so when flag is given, + rcc will never check possible newer version existence) +- bugfix: flag handling defaults on peek initialized flags +- typofix: on certificate appending failure message + ## v17.17.4 (date: 19.2.2024) - added `venv.md` for start of documentation for `rcc venv` and depxtraction diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 380a61aa..f24d64b0 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -115,6 +115,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["warranty-voided-mode"] = fmt.Sprintf("%v", common.WarrantyVoided()) result.Details["temp-management-disabled"] = fmt.Sprintf("%v", common.DisableTempManagement()) result.Details["pyc-management-disabled"] = fmt.Sprintf("%v", common.DisablePycManagement()) + result.Details["is-bundled"] = fmt.Sprintf("%v", common.IsBundled()) for name, filename := range lockfiles() { result.Details[name] = filename diff --git a/operations/rccversioncheck.go b/operations/rccversioncheck.go index 72ee20f7..b0547dae 100644 --- a/operations/rccversioncheck.go +++ b/operations/rccversioncheck.go @@ -87,6 +87,10 @@ func pickLatestTestedVersion(versions *rccVersions) (uint64, string, string) { } func RccVersionCheck() func() { + if common.IsBundled() { + common.Debug("Did not check newer version existence, since this is bundled case.") + return nil + } updateRccVersionInfo() versions, err := loadVersionsInfo() if err != nil || versions == nil { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6a1f9ac0..670a681d 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -14,6 +14,16 @@ Goal: Show rcc version information. Step build/rcc version --controller citests Must Have v17. +Goal: There is debug message when bundled case. + Step build/rcc version --controller citests --debug --bundled + Use STDERR + Must Have Did not check newer version existence, since this is bundled case. + +Goal: No debug message when user case. + Step build/rcc version --controller citests --debug + Use STDERR + Wont Have this is bundled case + Goal: Show rcc license information. Step build/rcc man license --controller citests Must Have Apache License diff --git a/settings/settings.go b/settings/settings.go index 5f7ea1d4..83a2e794 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -265,7 +265,7 @@ func (it gateway) loadRootCAs() *x509.CertPool { ok := roots.AppendCertsFromPEM(certificates) if !ok { - common.Log("Warning! Problem appending sertificated from %q.", common.CaBundleFile()) + common.Log("Warning! Problem appending certificates from %q.", common.CaBundleFile()) } return roots } From 11b02c05f4ec47dcbb58ad55259c8a2a398e9ee9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 4 Mar 2024 10:05:49 +0200 Subject: [PATCH 481/516] Bugfix: case-insensitive hash comparison (v17.18.1) - bugfix: template hash case-sensitivity fix --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/bugs_test.go | 26 ++++++++++++++++++++++++++ operations/initialize.go | 8 ++++++-- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 operations/bugs_test.go diff --git a/common/version.go b/common/version.go index d09c56ab..1051c358 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.18.0` + Version = `v17.18.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index a4ca9f2f..049db82f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.18.1 (date: 4.3.2024) + +- bugfix: template hash case-sensitivity fix + ## v17.18.0 (date: 23.2.2024) - new `--bundled` flag to support cases where rcc is bundled inside other apps diff --git a/operations/bugs_test.go b/operations/bugs_test.go new file mode 100644 index 00000000..d1789649 --- /dev/null +++ b/operations/bugs_test.go @@ -0,0 +1,26 @@ +package operations_test + +import ( + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/operations" +) + +func TestHashMatchingIsNotCaseSensitive(t *testing.T) { + must, wont := hamlet.Specifications(t) + + sut := operations.MetaTemplates{ + Hash: "\t\tCatsAndDogs\r\n", + } + + must.True(sut.MatchingHash(" catsanddogs ")) + wont.True(sut.MatchingHash(" dogsandcats ")) + + sut = operations.MetaTemplates{ + Hash: "catsanddogs", + } + + must.True(sut.MatchingHash(" CatsAndDogs ")) + wont.True(sut.MatchingHash(" dogsandcats ")) +} diff --git a/operations/initialize.go b/operations/initialize.go index 93da2f83..d18045c4 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -30,6 +30,10 @@ type MetaTemplates struct { Url string `yaml:"url"` } +func (it *MetaTemplates) MatchingHash(hash string) bool { + return strings.EqualFold(strings.TrimSpace(hash), strings.TrimSpace(it.Hash)) +} + func (it StringPairList) Len() int { return len(it) } @@ -92,7 +96,7 @@ func needNewTemplates() (ignore *MetaTemplates, err error) { fail.On(err != nil, "%s", err) fail.On(!strings.HasPrefix(meta.Url, "https:"), "Location for templates.zip is not https: %q", meta.Url) hash, err := pathlib.Sha256(TemplatesZip()) - if err != nil || hash != meta.Hash { + if err != nil || !meta.MatchingHash(hash) { return meta, nil } return nil, nil @@ -120,7 +124,7 @@ func downloadTemplatesZip(meta *MetaTemplates) (err error) { fail.On(err != nil, "Failure loading %q, reason: %s", meta.Url, err) hash, err := pathlib.Sha256(partfile) fail.On(err != nil, "Failure hashing %q, reason: %s", partfile, err) - fail.On(hash != meta.Hash, "Received broken templates.zip from %q", meta.Hash) + fail.On(!meta.MatchingHash(hash), "Received broken templates.zip, hash mismatch from expected %q vs. actual %q", meta.Hash, hash) return nil } From 167e95fedc65012945b63b19f85962b07e6b9d90 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 11 Mar 2024 16:36:14 +0200 Subject: [PATCH 482/516] Upgrade: micromamba v1.5.7 upgrade (v17.19.0) - micromamba upgrade to v1.5.7 --- assets/micromamba_version.txt | 2 +- blobs/embedded.go | 2 +- common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/micromamba_version.txt b/assets/micromamba_version.txt index c9b3c015..22708fe0 100644 --- a/assets/micromamba_version.txt +++ b/assets/micromamba_version.txt @@ -1 +1 @@ -v1.5.1 \ No newline at end of file +v1.5.7 diff --git a/blobs/embedded.go b/blobs/embedded.go index e1097385..e3d4bff1 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -8,7 +8,7 @@ import ( const ( // for micromamba upgrade, change following constants to match // and also remember to update assets/micromamba_version.txt to match this - MicromambaVersionLimit = 1_005_001 + MicromambaVersionLimit = 1_005_007 ) //go:embed assets/*.yaml docs/*.md diff --git a/common/version.go b/common/version.go index 1051c358..3612abc2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.18.1` + Version = `v17.19.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 049db82f..72f1be18 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.19.0 (date: 11.3.2024) + +- micromamba upgrade to v1.5.7 + ## v17.18.1 (date: 4.3.2024) - bugfix: template hash case-sensitivity fix From 3696ffd77aca19966bdef4bd1260fae43da30175 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 12 Mar 2024 17:01:49 +0200 Subject: [PATCH 483/516] Experiment: uv support (v17.20.0) - uv experiment with limited scope and imperfect implementation --- common/variables.go | 5 ++++ common/version.go | 2 +- conda/cleanup.go | 2 ++ conda/condayaml.go | 13 +++++++++++ conda/robocorp.go | 19 +++++++++++++++ conda/workflows.go | 56 ++++++++++++++++++++++++++++++++++++++++++++- docs/changelog.md | 4 ++++ 7 files changed, 99 insertions(+), 2 deletions(-) diff --git a/common/variables.go b/common/variables.go index 468267ec..3c0c796b 100644 --- a/common/variables.go +++ b/common/variables.go @@ -100,6 +100,7 @@ func init() { ensureDirectory(JournalLocation()) ensureDirectory(TemplateLocation()) ensureDirectory(BinLocation()) + ensureDirectory(UvCache()) ensureDirectory(PipCache()) ensureDirectory(WheelCache()) ensureDirectory(RobotCache()) @@ -288,6 +289,10 @@ func UsesHolotree() bool { return len(HolotreeSpace) > 0 } +func UvCache() string { + return filepath.Join(RobocorpHome(), "uvcache") +} + func PipCache() string { return filepath.Join(RobocorpHome(), "pipcache") } diff --git a/common/version.go b/common/version.go index 3612abc2..4de1c527 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.19.0` + Version = `v17.20.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 7eaaf79c..3b7a428f 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -71,10 +71,12 @@ func downloadCleanup(dryrun bool) error { if dryrun { common.Log("- %v", common.TemplateLocation()) common.Log("- %v", common.PipCache()) + common.Log("- %v", common.UvCache()) common.Log("- %v", common.MambaPackages()) } else { safeRemove("templates", common.TemplateLocation()) safeRemove("cache", common.PipCache()) + safeRemove("cache", common.UvCache()) safeRemove("cache", common.MambaPackages()) } return nil diff --git a/conda/condayaml.go b/conda/condayaml.go index 7311fd56..1220eb28 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -87,6 +87,10 @@ func (it *Dependency) IsCacheable() bool { return true } +func (it *Dependency) Match(name string) bool { + return strings.EqualFold(name, it.Name) +} + func (it *Dependency) IsExact() bool { return len(it.Qualifier)+len(it.Versions) > 0 } @@ -162,6 +166,15 @@ func SummonEnvironment(filename string) *Environment { } } +func (it *Environment) HasCondaDependency(name string) bool { + for _, dependency := range it.Conda { + if dependency.Match(name) { + return true + } + } + return false +} + func (it *Environment) IsCacheable() bool { for _, dependency := range it.Conda { if !dependency.IsCacheable() { diff --git a/conda/robocorp.go b/conda/robocorp.go index e7085e7f..bd5d109b 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -103,6 +103,15 @@ func FindPython(location string) (string, bool) { return holotreePath.Which("python", FileExtensions) } +func FindUv(location string) (string, bool) { + holotreePath := HolotreePath(location) + uv, ok := holotreePath.Which("uv", FileExtensions) + if ok { + return uv, ok + } + return holotreePath.Which("uv", FileExtensions) +} + func injectNetworkEnvironment(environment []string) []string { if settings.Global.NoRevocation() { environment = append(environment, "MAMBA_SSL_NO_REVOKE=true") @@ -227,6 +236,16 @@ search: return version, versionText } +func UvVersion(uv string) string { + environment := CondaExecutionEnvironment(".", nil, true) + versionText, _, err := shell.New(environment, ".", uv, "--version").CaptureOutput() + if err != nil { + return err.Error() + } + _, versionText = AsVersion(versionText) + return versionText +} + func PipVersion(python string) string { environment := CondaExecutionEnvironment(".", nil, true) versionText, _, err := shell.New(environment, ".", python, "-m", "pip", "--version").CaptureOutput() diff --git a/conda/workflows.go b/conda/workflows.go index c3f1f735..83c3a9e6 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -30,10 +30,13 @@ const ( const ( micromambaInstall = `micromamba install` pipInstall = `pip install` + uvInstall = `uv install` postInstallScripts = `post-install script execution` ) type ( + pipTool func(string, string, string, fmt.Stringer, io.Writer) (bool, bool, bool, string) + SkipLayer uint8 Recorder interface { Record([]byte) error @@ -205,6 +208,50 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. return true, false } +func uvLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer) (bool, bool, bool, string) { + assertStageFolder(targetFolder) + common.TimelineBegin("Layer: uv [%s]", fingerprint) + defer common.TimelineEnd() + + pipUsed := false + fmt.Fprintf(planWriter, "\n--- uv plan @%ss ---\n\n", stopwatch) + uv, uvok := FindUv(targetFolder) + if !uvok { + fmt.Fprintf(planWriter, "Note: no uv in target folder: %s\n", targetFolder) + return false, false, pipUsed, "" + } + python, pyok := FindPython(targetFolder) + if !pyok { + fmt.Fprintf(planWriter, "Note: no python in target folder: %s\n", targetFolder) + } + uvCache, wheelCache := common.UvCache(), common.WheelCache() + size, ok := pathlib.Size(requirementsText) + if !ok || size == 0 { + pretty.Progress(8, "Skipping pip install phase -- no pip dependencies.") + } else { + pretty.Progress(8, "Running uv install phase. (uv v%s) [layer: %s]", UvVersion(uv), fingerprint) + common.Debug("Updating new environment at %v with uv requirements from %v (size: %v)", targetFolder, requirementsText, size) + uvCommand := common.NewCommander(uv, "pip", "install", "--link-mode", "copy", "--color", "never", "--cache-dir", uvCache, "--find-links", wheelCache, "--requirement", requirementsText) + uvCommand.Option("--index-url", settings.Global.PypiURL()) + // no "--trusted-host" on uv pip install + // uvCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) + uvCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") + common.Debug("=== uv install phase ===") + code, err := LiveExecution(planWriter, targetFolder, uvCommand.CLI()...) + if err != nil || code != 0 { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.uv", fmt.Sprintf("%d_%x", code, code)) + common.Timeline("uv fail.") + common.Fatal(fmt.Sprintf("uv [%d/%x]", code, code), err) + pretty.RccPointOfView(uvInstall, err) + return false, false, pipUsed, "" + } + journal.CurrentBuildEvent().PipComplete() + common.Timeline("uv done.") + pipUsed = true + } + return true, false, pipUsed, python +} + func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer) (bool, bool, bool, string) { assertStageFolder(targetFolder) common.TimelineBegin("Layer: pip [%s]", fingerprint) @@ -290,6 +337,13 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t pipNeeded := len(requirementsText) > 0 postInstall := len(finalEnv.PostInstall) > 0 + var pypiSelector pipTool = pipLayer + + hasUv := finalEnv.HasCondaDependency("uv") + if hasUv { + pypiSelector = uvLayer + } + layers := finalEnv.AsLayers() fingerprints := finalEnv.FingerprintLayers() @@ -312,7 +366,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t fmt.Fprintf(planWriter, "\n--- micromamba plan skipped, layer exists ---\n\n") } if skip < SkipPipLayer { - success, fatal, pipUsed, python = pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter) + success, fatal, pipUsed, python = pypiSelector(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter) if !success { return success, fatal, pipUsed, python } diff --git a/docs/changelog.md b/docs/changelog.md index 72f1be18..8086c675 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.20.0 (date: 12.3.2024) WORK IN PROGRESS + +- uv experiment with limited scope and imperfect implementation + ## v17.19.0 (date: 11.3.2024) - micromamba upgrade to v1.5.7 From 604deef2a6ac9ad48aa0a78db0e35987eb506360 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 25 Mar 2024 14:40:57 +0200 Subject: [PATCH 484/516] Experiment: uncompressed hololib (v17.21.0) - experimental feature to disable compression of hololib content - made cleanup much more strict on error detection - bug fix: task shell was missing `--space` option; added now --- cmd/cleanup.go | 4 +++- cmd/shell.go | 1 + common/variables.go | 4 ++++ common/version.go | 2 +- conda/cleanup.go | 47 ++++++++++++++++++++++++++------------------- docs/changelog.md | 6 ++++++ htfs/commands.go | 2 +- htfs/delegates.go | 12 ++++++++---- htfs/functions.go | 18 ++++++++++++----- htfs/library.go | 7 ++++++- htfs/unmanaged.go | 4 ++++ htfs/virtual.go | 4 ++++ htfs/ziplibrary.go | 4 ++++ 13 files changed, 82 insertions(+), 33 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 994aeede..29066412 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -13,6 +13,7 @@ var ( quickFlag bool micromambaFlag bool downloadsFlag bool + noCompressFlag bool daysOption int ) @@ -25,7 +26,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag() { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag) + err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag, noCompressFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -40,5 +41,6 @@ func init() { cleanupCmd.Flags().BoolVarP(&allFlag, "all", "", false, "Cleanup all enviroments.") cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") cleanupCmd.Flags().BoolVarP(&downloadsFlag, "downloads", "", false, "Cleanup downloaded cache files (pip/conda/templates)") + cleanupCmd.Flags().BoolVarP(&noCompressFlag, "no-compress", "", false, "Do not use compression in hololib content. Experimental! DANGEROUS! Do not use, unless you know what you are doing.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep temp folders (deletes directories older than this).") } diff --git a/cmd/shell.go b/cmd/shell.go index 1f50a4e4..e6304fb1 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -34,6 +34,7 @@ func init() { shellCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") shellCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") + shellCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify used environment.") shellCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to configure shell from configuration file. ") shellCmd.MarkFlagRequired("config") } diff --git a/common/variables.go b/common/variables.go index 3c0c796b..0eb5f912 100644 --- a/common/variables.go +++ b/common/variables.go @@ -270,6 +270,10 @@ func HololibUsageLocation() string { return filepath.Join(HololibLocation(), "used") } +func HololibCompressMarker() string { + return filepath.Join(HololibCatalogLocation(), "compress.no") +} + func HolotreeLock() string { return filepath.Join(HolotreeLocation(), "global.lck") } diff --git a/common/version.go b/common/version.go index 4de1c527..5ab09d61 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.20.0` + Version = `v17.21.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 3b7a428f..1bd62aeb 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -6,6 +6,7 @@ import ( "time" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" ) @@ -67,17 +68,18 @@ func alwaysCleanup(dryrun bool) { safeRemove("legacy", miniconda3) } -func downloadCleanup(dryrun bool) error { +func downloadCleanup(dryrun bool) (err error) { + defer fail.Around(&err) if dryrun { common.Log("- %v", common.TemplateLocation()) common.Log("- %v", common.PipCache()) common.Log("- %v", common.UvCache()) common.Log("- %v", common.MambaPackages()) } else { - safeRemove("templates", common.TemplateLocation()) - safeRemove("cache", common.PipCache()) - safeRemove("cache", common.UvCache()) - safeRemove("cache", common.MambaPackages()) + fail.Fast(safeRemove("templates", common.TemplateLocation())) + fail.Fast(safeRemove("cache", common.PipCache())) + fail.Fast(safeRemove("cache", common.UvCache())) + fail.Fast(safeRemove("cache", common.MambaPackages())) } return nil } @@ -96,11 +98,10 @@ func quickCleanup(dryrun bool) error { return safeRemove("temp", common.RobocorpTempRoot()) } -func spotlessCleanup(dryrun bool) error { - err := quickCleanup(dryrun) - if err != nil { - return err - } +func spotlessCleanup(dryrun, noCompress bool) (err error) { + defer fail.Around(&err) + + fail.Fast(quickCleanup(dryrun)) rcccache := filepath.Join(common.RobocorpHome(), "rcccache.yaml") if dryrun { common.Log("- %v", common.BinLocation()) @@ -113,14 +114,18 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", common.HololibLocation()) return nil } - safeRemove("executables", common.BinLocation()) - safeRemove("micromamba", common.MicromambaLocation()) - safeRemove("cache", common.RobotCache()) - safeRemove("cache", rcccache) - safeRemove("old", common.OldEventJournal()) - safeRemove("journals", common.JournalLocation()) - safeRemove("catalogs", common.HololibCatalogLocation()) - return safeRemove("cache", common.HololibLocation()) + fail.Fast(safeRemove("executables", common.BinLocation())) + fail.Fast(safeRemove("micromamba", common.MicromambaLocation())) + fail.Fast(safeRemove("cache", common.RobotCache())) + fail.Fast(safeRemove("cache", rcccache)) + fail.Fast(safeRemove("old", common.OldEventJournal())) + fail.Fast(safeRemove("journals", common.JournalLocation())) + fail.Fast(safeRemove("catalogs", common.HololibCatalogLocation())) + fail.Fast(safeRemove("cache", common.HololibLocation())) + if noCompress { + return pathlib.WriteFile(common.HololibCompressMarker(), []byte("present"), 0o666) + } + return nil } func cleanupTemp(deadline time.Time, dryrun bool) error { @@ -160,7 +165,9 @@ func BugsCleanup() { bugsCleanup(false) } -func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error { +func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress bool) (err error) { + defer fail.Around(&err) + lockfile := common.RobocorpLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment cleanup [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000) @@ -183,7 +190,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error } if all { - return spotlessCleanup(dryrun) + return spotlessCleanup(dryrun, noCompress) } deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) diff --git a/docs/changelog.md b/docs/changelog.md index 8086c675..ee4a897e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.21.0 (date: 25.3.2024) WORK IN PROGRESS + +- experimental feature to disable compression of hololib content +- made cleanup much more strict on error detection +- bug fix: task shell was missing `--space` option; added now + ## v17.20.0 (date: 12.3.2024) WORK IN PROGRESS - uv experiment with limited scope and imperfect implementation diff --git a/htfs/commands.go b/htfs/commands.go index 2824dd78..08b7a86e 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -115,7 +115,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } if restore { - pretty.Progress(14, "Restore space from library [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) + pretty.Progress(14, "Restore space from library [with %d workers on %d CPUs; with compression: %v].", anywork.Scale(), runtime.NumCPU(), library.Compress()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() diff --git a/htfs/delegates.go b/htfs/delegates.go index d61935f0..998eac92 100644 --- a/htfs/delegates.go +++ b/htfs/delegates.go @@ -20,10 +20,14 @@ func gzDelegateOpen(filename string, ungzip bool) (readable io.Reader, closer Cl _, err = source.Seek(0, 0) fail.On(err != nil, "Failed to seek %q -> %v", filename, err) reader = source - } - closer = func() error { - reader.Close() - return source.Close() + closer = func() error { + return source.Close() + } + } else { + closer = func() error { + reader.Close() + return source.Close() + } } return reader, closer, nil } diff --git a/htfs/functions.go b/htfs/functions.go index 9ffb92cd..f01e5e81 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -182,6 +182,7 @@ detector: func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { var scheduler Treetop + compress := library.Compress() seen := make(map[string]bool) scheduler = func(path string, it *Dir) error { if it.IsSymlink() { @@ -211,14 +212,14 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { continue } sourcepath := filepath.Join(path, name) - anywork.Backlog(LiftFile(sourcepath, sinkpath)) + anywork.Backlog(LiftFile(sourcepath, sinkpath, compress)) } return nil } return scheduler } -func LiftFile(sourcename, sinkname string) anywork.Work { +func LiftFile(sourcename, sinkname string, compress bool) anywork.Work { return func() { source, err := os.Open(sourcename) anywork.OnErrPanicCloseAll(err) @@ -230,13 +231,20 @@ func LiftFile(sourcename, sinkname string) anywork.Work { anywork.OnErrPanicCloseAll(err) defer sink.Close() - writer, err := gzip.NewWriterLevel(sink, gzip.BestSpeed) - anywork.OnErrPanicCloseAll(err, sink) + + var writer io.WriteCloser + writer = sink + if compress { + writer, err = gzip.NewWriterLevel(sink, gzip.BestSpeed) + anywork.OnErrPanicCloseAll(err, sink) + } _, err = io.Copy(writer, source) anywork.OnErrPanicCloseAll(err, sink) - anywork.OnErrPanicCloseAll(writer.Close(), sink) + if compress { + anywork.OnErrPanicCloseAll(writer.Close(), sink) + } anywork.OnErrPanicCloseAll(sink.Close()) diff --git a/htfs/library.go b/htfs/library.go index 719d15a1..b924eac6 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -59,6 +59,7 @@ type Library interface { TargetDir([]byte, []byte, []byte) (string, error) Restore([]byte, []byte, []byte) (string, error) RestoreTo([]byte, string, string, string, bool) (string, error) + Compress() bool } type MutableLibrary interface { @@ -82,7 +83,7 @@ type hololib struct { } func (it *hololib) Open(digest string) (readable io.Reader, closer Closer, err error) { - return delegateOpen(it, digest, true) + return delegateOpen(it, digest, it.Compress()) } func (it *hololib) Location(digest string) string { @@ -268,6 +269,10 @@ func (it *hololib) ValidateBlueprint(blueprint []byte) error { return nil } +func (it *hololib) Compress() bool { + return !pathlib.IsFile(common.HololibCompressMarker()) +} + func (it *hololib) HasBlueprint(blueprint []byte) bool { key := common.BlueprintHash(blueprint) found, ok := it.queryCache[key] diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index e594ed54..2465596e 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -26,6 +26,10 @@ func Unmanaged(core MutableLibrary) MutableLibrary { } } +func (it *unmanaged) Compress() bool { + return it.delegate.Compress() +} + func (it *unmanaged) Identity() string { return it.delegate.Identity() } diff --git a/htfs/virtual.go b/htfs/virtual.go index 1d2dbc22..334adf45 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -26,6 +26,10 @@ func Virtual() MutableLibrary { } } +func (it *virtual) Compress() bool { + return true +} + func (it *virtual) Identity() string { suffix := fmt.Sprintf("%016x", it.identity) return fmt.Sprintf("v%s_%sh", common.UserHomeIdentity(), suffix[:14]) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 758582a6..e17497ce 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -40,6 +40,10 @@ func ZipLibrary(zipfile string) (Library, error) { }, nil } +func (it *ziplibrary) Compress() bool { + return true +} + func (it *ziplibrary) ValidateBlueprint(blueprint []byte) error { return nil } From 44e062d42572acdb9896ea25585451cde01a06a9 Mon Sep 17 00:00:00 2001 From: raivolink <75722172+raivolink@users.noreply.github.com> Date: Sun, 24 Mar 2024 15:23:06 +0200 Subject: [PATCH 485/516] Update links to new documentation site --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5742480a..dcc929d0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ RCC allows you to create, manage, and distribute Python-based self-contained aut 🚀 "Repeatable, movable and isolated Python environments for your automation." -Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) configuration file, `rcc` is a foundation that allows anyone to build and share automation easily. +Together with [robot.yaml](https://robocorp.com/docs/robot-structure/robot-yaml-format) configuration file, `rcc` is a foundation that allows anyone to build and share automation easily.

@@ -41,7 +41,7 @@ RCC is actively maintained by [Robocorp](https://www.robocorp.com/). :hatching_chick: Create your own robot from templates > `rcc create` -For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.com/docs/product-manuals/robocorp-cli) to get started. To build `rcc` from this repository, see the [Setup Guide](/docs/BUILD.md) +For detailed instructions, visit [Robocorp RCC documentation](https://robocorp.com/docs/rcc/overview) to get started. To build `rcc` from this repository, see the [Setup Guide](/docs/BUILD.md) ## Installing RCC from the command line @@ -87,8 +87,6 @@ These are also visible inside RCC using the command: `rcc docs recipes`. The Robocorp community can be found on [Developer Slack](https://robocorp-developers.slack.com), where you can ask questions, voice ideas, and share your projects. -You can also use the [Robocorp Forum](https://forum.robocorp.com) - ## License Apache 2.0 From f0ab17ec6c2bb355ce04eed55fe86767ba170562 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 27 Mar 2024 16:09:44 +0200 Subject: [PATCH 486/516] Experiment: siphash as hasher (v17.22.0) - compression flag is now globally accessible - compression flag also switches using siphash as identity hasher - dirtyness stats also now lists duplicates and linked files --- anywork/worker.go | 3 ++- common/algorithms.go | 13 +++++++++++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/commands.go | 2 +- htfs/functions.go | 15 ++++++++------- htfs/library.go | 34 +++++++++++++++++++++++++++------- htfs/relocator.go | 9 +++++---- htfs/unmanaged.go | 4 ---- htfs/ziplibrary.go | 4 ---- 10 files changed, 61 insertions(+), 31 deletions(-) diff --git a/anywork/worker.go b/anywork/worker.go index 33294ce9..47831fb5 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -97,7 +97,8 @@ func Backlog(todo Work) { } func Sync() error { - for retries := 0; retries < 10; retries++ { + trials := int(Scale()) + for retries := 0; retries < trials; retries++ { runtime.Gosched() } group.Wait() diff --git a/common/algorithms.go b/common/algorithms.go index 133e7cbe..bb960dad 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -3,6 +3,7 @@ package common import ( "crypto/sha256" "fmt" + "hash" "math" "math/rand" "time" @@ -35,15 +36,23 @@ func Hexdigest(raw []byte) string { return fmt.Sprintf("%02x", raw) } +func NewDigester(legacy bool) hash.Hash { + if legacy { + return sha256.New() + } else { + return siphash.New128([]byte("Very_Random-Seed")) + } +} + func ShortDigest(content string) string { - digester := sha256.New() + digester := NewDigester(true) digester.Write([]byte(content)) result := Hexdigest(digester.Sum(nil)) return result[:16] } func Digest(content string) string { - digester := sha256.New() + digester := NewDigester(true) digester.Write([]byte(content)) return Hexdigest(digester.Sum(nil)) } diff --git a/common/version.go b/common/version.go index 5ab09d61..ff13ec35 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.21.0` + Version = `v17.22.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index ee4a897e..d189b9be 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.22.0 (date: 27.3.2024) WORK IN PROGRESS + +- compression flag is now globally accessible +- compression flag also switches using siphash as identity hasher +- dirtyness stats also now lists duplicates and linked files + ## v17.21.0 (date: 25.3.2024) WORK IN PROGRESS - experimental feature to disable compression of hololib content diff --git a/htfs/commands.go b/htfs/commands.go index 08b7a86e..9b820b55 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -115,7 +115,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } if restore { - pretty.Progress(14, "Restore space from library [with %d workers on %d CPUs; with compression: %v].", anywork.Scale(), runtime.NumCPU(), library.Compress()) + pretty.Progress(14, "Restore space from library [with %d workers on %d CPUs; with compression: %v].", anywork.Scale(), runtime.NumCPU(), Compress()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() diff --git a/htfs/functions.go b/htfs/functions.go index f01e5e81..6110f5a4 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -2,7 +2,6 @@ package htfs import ( "compress/gzip" - "crypto/sha256" "fmt" "io" "os" @@ -113,7 +112,7 @@ func CheckHasher(known map[string]map[string]bool) Filetask { fail.On(err != nil, "Failed to seek %q -> %v", fullpath, err) reader = source } - digest := sha256.New() + digest := common.NewDigester(Compress()) _, err = io.Copy(digest, reader) if err != nil { anywork.Backlog(RemoveFile(fullpath)) @@ -132,7 +131,7 @@ func Locator(seek string) Filetask { panic(fmt.Sprintf("Open[Locator] %q, reason: %v", fullpath, err)) } defer source.Close() - digest := sha256.New() + digest := common.NewDigester(Compress()) locator := RelocateWriter(digest, seek) _, err = io.Copy(locator, source) if err != nil { @@ -182,7 +181,7 @@ detector: func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { var scheduler Treetop - compress := library.Compress() + compress := Compress() seen := make(map[string]bool) scheduler = func(path string, it *Dir) error { if it.IsSymlink() { @@ -193,10 +192,12 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { } for name, file := range it.Files { if file.IsSymlink() { + stats.Link() continue } if seen[file.Digest] { common.Trace("LiftFile %s %q already scheduled.", file.Digest, name) + stats.Duplicate() continue } seen[file.Digest] = true @@ -270,7 +271,7 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ sink, err := os.Create(partname) anywork.OnErrPanicCloseAll(err) - digester := sha256.New() + digester := common.NewDigester(Compress()) many := io.MultiWriter(sink, digester) _, err = io.Copy(many, reader) @@ -386,7 +387,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat } link, ok := it.Dirs[part.Name()] if ok && link.IsSymlink() { - stats.Dirty(false) + stats.Link() continue } files[part.Name()] = true @@ -398,7 +399,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat continue } if found.IsSymlink() && isCorrectSymlink(found.Symlink, directpath) { - stats.Dirty(false) + stats.Link() continue } shadow, ok := current[directpath] diff --git a/htfs/library.go b/htfs/library.go index b924eac6..6fe95cab 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -30,15 +30,36 @@ var ( type stats struct { sync.Mutex - total uint64 - dirty uint64 + total uint64 + dirty uint64 + links uint64 + duplicate uint64 } func (it *stats) Dirtyness() float64 { + it.Lock() + defer it.Unlock() + dirtyness := (1000 * it.dirty) / it.total return float64(dirtyness) / 10.0 } +func (it *stats) Duplicate() { + it.Lock() + defer it.Unlock() + + it.total++ + it.duplicate++ +} + +func (it *stats) Link() { + it.Lock() + defer it.Unlock() + + it.total++ + it.links++ +} + func (it *stats) Dirty(dirty bool) { it.Lock() defer it.Unlock() @@ -59,7 +80,6 @@ type Library interface { TargetDir([]byte, []byte, []byte) (string, error) Restore([]byte, []byte, []byte) (string, error) RestoreTo([]byte, string, string, string, bool) (string, error) - Compress() bool } type MutableLibrary interface { @@ -83,7 +103,7 @@ type hololib struct { } func (it *hololib) Open(digest string) (readable io.Reader, closer Closer, err error) { - return delegateOpen(it, digest, it.Compress()) + return delegateOpen(it, digest, Compress()) } func (it *hololib) Location(digest string) string { @@ -252,7 +272,7 @@ func (it *hololib) Record(blueprint []byte) error { common.Timeline("holotree lift start %q", catalog) err = fs.Treetop(ScheduleLifters(it, score)) common.Timeline("holotree lift done") - defer common.Timeline("- new %d/%d", score.dirty, score.total) + defer common.Timeline("- new %d/%d (duplicate: %d, links: %d)", score.dirty, score.total, score.duplicate, score.links) common.Debug("Holotree new workload: %d/%d\n", score.dirty, score.total) return err } @@ -269,7 +289,7 @@ func (it *hololib) ValidateBlueprint(blueprint []byte) error { return nil } -func (it *hololib) Compress() bool { +func Compress() bool { return !pathlib.IsFile(common.HololibCompressMarker()) } @@ -409,7 +429,7 @@ func (it *hololib) RestoreTo(blueprint []byte, label, controller, space string, err = fs.AllDirs(RestoreDirectory(it, fs, currentstate, score)) fail.On(err != nil, "Failed to restore directories -> %v", err) common.TimelineEnd() - defer common.Timeline("- dirty %d/%d", score.dirty, score.total) + defer common.Timeline("- dirty %d/%d (duplicate: %d, links: %d)", score.dirty, score.total, score.duplicate, score.links) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) journal.CurrentBuildEvent().Dirty(score.Dirtyness()) fs.Controller = controller diff --git a/htfs/relocator.go b/htfs/relocator.go index d58ac448..8d3ca1d6 100644 --- a/htfs/relocator.go +++ b/htfs/relocator.go @@ -27,7 +27,7 @@ func RelocateWriter(delegate io.Writer, needle string) WriteLocator { windowsize := len(blob) result := &simple{ windowsize: windowsize, - window: []byte{}, + window: make([]byte, 0, 8192+windowsize), trigger: blob[windowsize-1], needle: blob, history: 0, @@ -38,9 +38,10 @@ func RelocateWriter(delegate io.Writer, needle string) WriteLocator { } func (it *simple) trimWindow() { - total := len(it.window) - if total > it.windowsize { - it.window = it.window[total-it.windowsize:] + shift := len(it.window) - it.windowsize + if shift > 0 { + copy(it.window, it.window[shift:]) + it.window = it.window[:it.windowsize] } } diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index 2465596e..e594ed54 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -26,10 +26,6 @@ func Unmanaged(core MutableLibrary) MutableLibrary { } } -func (it *unmanaged) Compress() bool { - return it.delegate.Compress() -} - func (it *unmanaged) Identity() string { return it.delegate.Identity() } diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index e17497ce..758582a6 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -40,10 +40,6 @@ func ZipLibrary(zipfile string) (Library, error) { }, nil } -func (it *ziplibrary) Compress() bool { - return true -} - func (it *ziplibrary) ValidateBlueprint(blueprint []byte) error { return nil } From 154e995ad77bf7429ade6b01df69b6d54657da36 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 10 Apr 2024 11:47:16 +0300 Subject: [PATCH 487/516] Feature: better cleanup procedure (v17.23.0) - cleanup improvement with option `--caches` to remove conda/pip/uv/hololib caches but leave holotree available - also environment building now cleans up "building" space both before and after environment is build --- cmd/cleanup.go | 4 +++- cmd/holotreeVenv.go | 2 +- common/version.go | 2 +- conda/cleanup.go | 16 +++++++++++++++- docs/changelog.md | 7 +++++++ htfs/commands.go | 9 ++++++--- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 29066412..b837d651 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -11,6 +11,7 @@ import ( var ( allFlag bool quickFlag bool + cachesFlag bool micromambaFlag bool downloadsFlag bool noCompressFlag bool @@ -26,7 +27,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag() { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag, noCompressFlag) + err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag, noCompressFlag, cachesFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -37,6 +38,7 @@ After cleanup, they will not be available anymore.`, func init() { configureCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") + cleanupCmd.Flags().BoolVarP(&cachesFlag, "caches", "", false, "Just delete all caches (hololib/conda/uv/pip) but not holotree spaces. DANGEROUS! Do not use, unless you know what you are doing.") cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "", false, "Cleanup all enviroments.") cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") diff --git a/cmd/holotreeVenv.go b/cmd/holotreeVenv.go index ce3e0a88..742a3cf3 100644 --- a/cmd/holotreeVenv.go +++ b/cmd/holotreeVenv.go @@ -108,8 +108,8 @@ func listActivationScripts(root string) string { } func init() { - holotreeCmd.AddCommand(holotreeVenvCmd) rootCmd.AddCommand(holotreeVenvCmd) + holotreeCmd.AddCommand(holotreeVenvCmd) holotreeVenvCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") holotreeVenvCmd.Flags().BoolVarP(&holotreeForce, "force", "f", false, "Force environment creation by deleting unmanaged space. Dangerous, do not use unless you understand what it means.") diff --git a/common/version.go b/common/version.go index ff13ec35..f037676e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.22.0` + Version = `v17.23.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 1bd62aeb..7a9dfd62 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -98,6 +98,16 @@ func quickCleanup(dryrun bool) error { return safeRemove("temp", common.RobocorpTempRoot()) } +func cleanupAllCaches(dryrun bool) error { + downloadCleanup(dryrun) + if dryrun { + common.Log("- %v", common.HololibLocation()) + return nil + } + fail.Fast(safeRemove("cache", common.HololibLocation())) + return nil +} + func spotlessCleanup(dryrun, noCompress bool) (err error) { defer fail.Around(&err) @@ -165,7 +175,7 @@ func BugsCleanup() { bugsCleanup(false) } -func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress bool) (err error) { +func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress, caches bool) (err error) { defer fail.Around(&err) lockfile := common.RobocorpLock() @@ -189,6 +199,10 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress return quickCleanup(dryrun) } + if caches { + fail.Fast(cleanupAllCaches(dryrun)) + } + if all { return spotlessCleanup(dryrun, noCompress) } diff --git a/docs/changelog.md b/docs/changelog.md index d189b9be..0b170fe5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.23.0 (date: 10.4.2024) + +- cleanup improvement with option `--caches` to remove conda/pip/uv/hololib + caches but leave holotree available +- also environment building now cleans up "building" space both before and + after environment is build + ## v17.22.0 (date: 27.3.2024) WORK IN PROGRESS - compression flag is now globally accessible diff --git a/htfs/commands.go b/htfs/commands.go index 9b820b55..f653e161 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -125,14 +125,17 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal externally, err = conda.ApplyExternallyManaged(path) fail.Fast(err) + fail.Fast(CleanupHolotreeStage(tree)) return path, scorecard, nil } func CleanupHolotreeStage(tree MutableLibrary) error { - common.Timeline("holotree stage removal start") - defer common.Timeline("holotree stage removal done") - return pathlib.TryRemoveAll("stage", tree.Stage()) + common.TimelineBegin("holotree stage removal") + defer common.TimelineEnd() + location := tree.Stage() + common.Timeline("- removing %q", location) + return pathlib.TryRemoveAll("stage", location) } func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorecard common.Scorecard, puller CatalogPuller) (err error) { From 5cbe63832d521a97770736c47449a12d73946042 Mon Sep 17 00:00:00 2001 From: raivolink <75722172+raivolink@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:30:42 +0300 Subject: [PATCH 488/516] docs: fix obsolete data * Fixed link to long paths documentation * Removed Automation Studio mention --- conda/platform_windows.go | 2 +- docs/maintenance.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 7f34f4b2..14d1f77d 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -86,7 +86,7 @@ func HasLongPathSupport() bool { code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).StderrOnly().Transparent() common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) if err != nil { - longPathSupportArticle := settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") + longPathSupportArticle := settings.Global.DocsLink("troubleshooting/windows-long-path") common.Log("%sWARNING! Long path support failed. Reason: %v.%s", pretty.Red, err, pretty.Reset) common.Log("%sWARNING! See %v for more details.%s", pretty.Red, longPathSupportArticle, pretty.Reset) return false diff --git a/docs/maintenance.md b/docs/maintenance.md index 9c10a81d..7f2e2a5a 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -24,7 +24,7 @@ environments and catalogs that your maintenance targets to. ## Maintenance vs. tools using holotrees When doing maintenance on any holotree, you should be aware, that if Robocorp -tooling (Worker, Assistant, Automation Studio, VS Code plugins, rcc, ...) is +tooling (Worker, Assistant, VS Code plugins, rcc, ...) is also at same time using same holotree/hololib, your maintenance actions might have negative effect on those tools. From b1698aa9c8b4b75677510e059c543eba7de313c6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 15 Apr 2024 09:32:45 +0300 Subject: [PATCH 489/516] CI: GH action upgrade (v17.23.1) --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/rcc.yaml | 10 +++++----- common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 438a3150..e9e5d4a2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index f00d615b..0ac5925a 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -13,13 +13,13 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: go-version: '1.20.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: What run: rake what - name: Building @@ -33,16 +33,16 @@ jobs: matrix: os: ['ubuntu'] steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: go-version: '1.20.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup run: rake robotsetup - name: What diff --git a/common/version.go b/common/version.go index f037676e..7145616f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.23.0` + Version = `v17.23.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0b170fe5..209ad3ca 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.23.1 (date: 15.4.2024) + +- github action upgrades + ## v17.23.0 (date: 10.4.2024) - cleanup improvement with option `--caches` to remove conda/pip/uv/hololib From 3550bbd46c642db063197193ca59323b4a2dc9fa Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 15 Apr 2024 09:46:47 +0300 Subject: [PATCH 490/516] CI: more GH action upgrade (v17.23.2) --- .github/workflows/codeql-analysis.yml | 2 +- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e9e5d4a2..b0330eac 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/common/version.go b/common/version.go index 7145616f..870bbbe6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.23.1` + Version = `v17.23.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 209ad3ca..76c06bef 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.23.2 (date: 15.4.2024) + +- more github action upgrades + ## v17.23.1 (date: 15.4.2024) - github action upgrades From 99b7d2c0f906d251f2f1255f587feac06152bca6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 15 Apr 2024 10:47:19 +0300 Subject: [PATCH 491/516] Upgrade: micromamba to v1.5.8 (v17.24.0) - micromamba upgrade to v1.5.8 --- assets/micromamba_version.txt | 2 +- blobs/embedded.go | 2 +- common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/micromamba_version.txt b/assets/micromamba_version.txt index 22708fe0..1097babb 100644 --- a/assets/micromamba_version.txt +++ b/assets/micromamba_version.txt @@ -1 +1 @@ -v1.5.7 +v1.5.8 diff --git a/blobs/embedded.go b/blobs/embedded.go index e3d4bff1..808fe028 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -8,7 +8,7 @@ import ( const ( // for micromamba upgrade, change following constants to match // and also remember to update assets/micromamba_version.txt to match this - MicromambaVersionLimit = 1_005_007 + MicromambaVersionLimit = 1_005_008 ) //go:embed assets/*.yaml docs/*.md diff --git a/common/version.go b/common/version.go index 870bbbe6..cb04a15b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.23.2` + Version = `v17.24.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 76c06bef..b9c6eb61 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.24.0 (date: 15.4.2024) + +- micromamba upgrade to v1.5.8 + ## v17.23.2 (date: 15.4.2024) - more github action upgrades From 561bea82c32bf5f080c69230ebd601f736b6dc83 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Apr 2024 08:51:24 +0300 Subject: [PATCH 492/516] Bugfix: layer retry build bug (v17.25.0) - bug: when first build failed, original layers were expected to still be there - fix: now second build always builds all layers (since failure needs that) --- common/version.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index cb04a15b..06f9cc6d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.24.0` + Version = `v17.25.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 83c3a9e6..8e79d54a 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -155,7 +155,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall if err != nil { return false, err } - success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, skip, finalEnv, recorder) + success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, SkipNoLayers, finalEnv, recorder) } if success { journal.CurrentBuildEvent().Successful() diff --git a/docs/changelog.md b/docs/changelog.md index b9c6eb61..b5087141 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.25.0 (date: 17.4.2024) + +- bug: when first build failed, original layers were expected to still be there +- fix: now second build always builds all layers (since failure needs that) + ## v17.24.0 (date: 15.4.2024) - micromamba upgrade to v1.5.8 From e8878e75a46c94f19596a8bec8688bafff9dde7a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Apr 2024 10:38:13 +0300 Subject: [PATCH 493/516] Feature: no retry build flag (v17.26.0) - feature: `--no-retry-build` flag for tools to prevent rcc doing retry environment build in case of first build fails --- cmd/root.go | 1 + common/variables.go | 1 + common/version.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 5 +++++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index af889116..b425d0f6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -112,6 +112,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&anythingIgnore, "anything", "", "freeform string value that can be set without any effect, for example CLI versioning/reference") rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib (also RCC_NO_BUILD=1)") + rootCmd.PersistentFlags().BoolVarP(&common.NoRetryBuild, "no-retry-build", "", false, "no retry in case of first environment build fails, just report error immediately") rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output (also RCC_VERBOSITY=silent)") rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") rootCmd.PersistentFlags().BoolVarP(&pathlib.Lockless, "lockless", "", false, "do not use file locking ... DANGER!") diff --git a/common/variables.go b/common/variables.go index 0eb5f912..1bc6525a 100644 --- a/common/variables.go +++ b/common/variables.go @@ -40,6 +40,7 @@ const ( var ( NoBuild bool + NoRetryBuild bool NoTempManagement bool NoPycManagement bool ExternallyManaged bool diff --git a/common/version.go b/common/version.go index 06f9cc6d..2b6bc44c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.25.0` + Version = `v17.26.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 8e79d54a..51077aa0 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -144,7 +144,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall common.Debug("=== first try phase ===") common.Timeline("first try.") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, skip, finalEnv, recorder) - if !success && !force && !fatal { + if !success && !force && !fatal && !common.NoRetryBuild { journal.CurrentBuildEvent().Rebuild() cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) common.Debug("=== second try phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index b5087141..9fa71004 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.26.0 (date: 17.4.2024) + +- feature: `--no-retry-build` flag for tools to prevent rcc doing retry + environment build in case of first build fails + ## v17.25.0 (date: 17.4.2024) - bug: when first build failed, original layers were expected to still be there From 1613b93fdebb48c6269fd7368e879dd57889fd71 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Apr 2024 15:20:03 +0300 Subject: [PATCH 494/516] Feature: use feature truststore is cacheable (v17.27.0) - when pip dependencies has `--use-feature=truststore` those environments are identified as cacheable - removed some robot.yaml file diagnostic checks since those are not valid anymore --- common/version.go | 2 +- conda/condayaml.go | 22 +++++++++++++++++----- conda/condayaml_test.go | 1 + docs/changelog.md | 7 +++++++ robot/robot.go | 20 +++++++++----------- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/common/version.go b/common/version.go index 2b6bc44c..ae1ff036 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.26.0` + Version = `v17.27.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 1220eb28..712c2bf4 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -14,11 +14,12 @@ import ( ) const ( - alternative = `|` - reject_chars = `(?:[][(){}%/:,;@*<=>!]+)` - and_or = `\b(?:or|and)\b` - dash_start = `^-+` - uncacheableForm = reject_chars + alternative + and_or + alternative + dash_start + useFeatureTruststore = `--use-feature=truststore` + alternative = `|` + reject_chars = `(?:[][(){}%/:,;@*<=>!]+)` + and_or = `\b(?:or|and)\b` + dash_start = `^-+` + uncacheableForm = reject_chars + alternative + and_or + alternative + dash_start ) var ( @@ -64,6 +65,11 @@ func AsDependency(value string) *Dependency { } } +func IsSpecialCacheable(name, version string) bool { + flat := fmt.Sprintf("%s=%s", strings.TrimSpace(name), strings.TrimSpace(version)) + return strings.EqualFold(flat, useFeatureTruststore) +} + func IsCacheable(text string) bool { flat := strings.TrimSpace(text) return !uncacheablePattern.MatchString(flat) @@ -75,6 +81,9 @@ func (it *Dependency) Representation() string { } func (it *Dependency) IsCacheable() bool { + if IsSpecialCacheable(it.Name, it.Versions) { + return true + } if !it.IsExact() { return false } @@ -609,6 +618,9 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b } ok = true for _, dependency := range it.Pip { + if IsSpecialCacheable(dependency.Name, dependency.Versions) { + continue + } presentation := dependency.Representation() if packages[presentation] { notice(0, "", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index 87b3094d..fdd00fc4 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -159,6 +159,7 @@ func TestCacheability(t *testing.T) { must_be.True(conda.IsCacheable("2023c")) must_be.True(conda.IsCacheable("2023.3")) must_be.True(conda.IsCacheable("0.1.0.post0")) + must_be.True(conda.IsSpecialCacheable("--use-feature", "truststore")) wont_be.True(conda.IsCacheable("a,b")) wont_be.True(conda.IsCacheable("simple or not")) diff --git a/docs/changelog.md b/docs/changelog.md index 9fa71004..6bd79924 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.27.0 (date: 17.4.2024) + +- when pip dependencies has `--use-feature=truststore` those environments + are identified as cacheable +- removed some robot.yaml file diagnostic checks since those are not valid + anymore + ## v17.26.0 (date: 17.4.2024) - feature: `--no-retry-build` flag for tools to prevent rcc doing retry diff --git a/robot/robot.go b/robot/robot.go index d993be0b..90913e3b 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -203,19 +203,17 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose.Ok(0, "Artifacts directory defined in robot.yaml") } } - if it.Conda == "" { - diagnose.Ok(0, "In robot.yaml, 'condaConfigFile:' is missing. So this is shell robot.") + effectiveConfig := it.CondaConfigFile() + if effectiveConfig == "" { + diagnose.Ok(0, "In robot.yaml, effective environment configuration is missing. So this is shell robot.") + target.Details["cacheable-environment-configuration"] = "false" } else { - if filepath.IsAbs(it.Conda) { - diagnose.Fail(0, "", "condaConfigFile %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) + diagnose.Ok(0, "In robot.yaml, environment configuration %q is present. So this is python robot.", effectiveConfig) + condaEnv, err := conda.ReadCondaYaml(effectiveConfig) + if err != nil { + diagnose.Fail(0, "", "From robot.yaml, loading conda.yaml failed with: %v", err) } else { - diagnose.Ok(0, "In robot.yaml, 'condaConfigFile:' is present. So this is python robot.") - condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) - if err != nil { - diagnose.Fail(0, "", "From robot.yaml, loading conda.yaml failed with: %v", err) - } else { - condaEnv.Diagnostics(target, production) - } + condaEnv.Diagnostics(target, production) } } target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) From 1ffed49f4b636b89b6d1a284f1c7a789440ce6c3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 22 Apr 2024 11:47:18 +0300 Subject: [PATCH 495/516] Feature: package.yaml as conda.yaml replacement (v17.28.0) - adding support for `package.yaml` as replacement for `conda.yaml` --- common/version.go | 2 +- conda/packageyaml.go | 103 +++++++++++++++++++++++++++++++++++++++++++ docs/changelog.md | 4 ++ htfs/commands.go | 2 +- 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 conda/packageyaml.go diff --git a/common/version.go b/common/version.go index ae1ff036..218afd8e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.27.0` + Version = `v17.28.0` ) diff --git a/conda/packageyaml.go b/conda/packageyaml.go new file mode 100644 index 00000000..2a635248 --- /dev/null +++ b/conda/packageyaml.go @@ -0,0 +1,103 @@ +package conda + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/pathlib" + "gopkg.in/yaml.v2" +) + +type ( + packageDependencies struct { + CondaForge []string `yaml:"conda-forge,omitempty"` + Pypi []string `yaml:"pypi,omitempty"` + } + internalPackage struct { + Dependencies *packageDependencies `yaml:"dependencies"` + PostInstall []string `yaml:"post-install,omitempty"` + } +) + +func (it *internalPackage) AsEnvironment() *Environment { + result := &Environment{ + Channels: []string{"conda-forge"}, + PostInstall: []string{}, + } + seenScripts := make(map[string]bool) + result.PostInstall = addItem(seenScripts, it.PostInstall, result.PostInstall) + pushConda(result, it.condaDependencies()) + pushPip(result, it.pipDependencies()) + result.pipPromote() + return result +} + +func fixPipDependency(dependency *Dependency) *Dependency { + if dependency != nil { + if dependency.Qualifier == "=" { + dependency.Original = fmt.Sprintf("%s==%s", dependency.Name, dependency.Versions) + dependency.Qualifier = "==" + } + } + return dependency +} + +func (it *internalPackage) pipDependencies() []*Dependency { + result := make([]*Dependency, 0, len(it.Dependencies.Pypi)) + for _, item := range it.Dependencies.Pypi { + dependency := AsDependency(item) + if dependency != nil { + result = append(result, fixPipDependency(dependency)) + } + } + return result +} + +func (it *internalPackage) condaDependencies() []*Dependency { + result := make([]*Dependency, 0, len(it.Dependencies.CondaForge)) + for _, item := range it.Dependencies.CondaForge { + dependency := AsDependency(item) + if dependency != nil { + result = append(result, dependency) + } + } + return result +} + +func packageYamlFrom(content []byte) (*Environment, error) { + result := new(internalPackage) + err := yaml.Unmarshal(content, result) + if err != nil { + return nil, err + } + return result.AsEnvironment(), nil +} + +func ReadPackageCondaYaml(filename string) (*Environment, error) { + basename := strings.ToLower(filepath.Base(filename)) + if basename == "package.yaml" { + environment, err := ReadPackageYaml(filename) + if err == nil { + return environment, nil + } + } + return ReadCondaYaml(filename) +} + +func ReadPackageYaml(filename string) (*Environment, error) { + var content []byte + var err error + + if pathlib.IsFile(filename) { + content, err = os.ReadFile(filename) + } else { + content, err = cloud.ReadFile(filename) + } + if err != nil { + return nil, fmt.Errorf("%q: %w", filename, err) + } + return packageYamlFrom(content) +} diff --git a/docs/changelog.md b/docs/changelog.md index 6bd79924..375da890 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.28.0 (date: 22.4.2024) + +- adding support for `package.yaml` as replacement for `conda.yaml` + ## v17.27.0 (date: 17.4.2024) - when pip dependencies has `--use-feature=truststore` those environments diff --git a/htfs/commands.go b/htfs/commands.go index f653e161..8b3597c9 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -261,7 +261,7 @@ func ComposeFinalBlueprint(userFiles []string, packfile string) (config robot.Ro for _, filename := range filenames { left = right - right, err = conda.ReadCondaYaml(filename) + right, err = conda.ReadPackageCondaYaml(filename) fail.On(err != nil, "Failure: %v", err) if left == nil { continue From 1ed386bd40220daf923df08521575a7eb8b90a80 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Apr 2024 11:27:38 +0300 Subject: [PATCH 496/516] Bugfix: layers missing from export (v17.28.1) - bugfix: when exporting prebuild environments, include layer catalogs also - bugfix: exporting was not adding all environments correctly to .zip file --- cmd/holotreePrebuild.go | 20 ++++++++++++++++---- common/version.go | 2 +- conda/workflows.go | 6 +++--- docs/changelog.md | 5 +++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index 14f4b302..f15f04b5 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -8,10 +8,12 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" "github.com/spf13/cobra" ) @@ -111,19 +113,29 @@ var holotreePrebuildCmd = &cobra.Command{ configurations := metafileExpansion(args, metafileFlag) total, failed := len(configurations), 0 success := make([]string, 0, total) + exporting := len(exportFile) > 0 for at, configfile := range configurations { + environment, err := conda.ReadPackageCondaYaml(configfile) + if err != nil { + pretty.Warning("%d/%d: Failed to load %q, reason: %v (ignored)", at+1, total, configfile, err) + continue + } pretty.Note("%d/%d: Now building config %q", at+1, total, configfile) - _, _, err := htfs.NewEnvironment(configfile, "", false, forceBuild, operations.PullCatalog) + _, _, err = htfs.NewEnvironment(configfile, "", false, forceBuild, operations.PullCatalog) if err != nil { failed += 1 pretty.Warning("%d/%d: Holotree recording error: %v", at+1, total, err) } else { - if common.FreshlyBuildEnvironment { - success = append(success, htfs.CatalogName(common.EnvironmentHash)) + for _, hash := range environment.FingerprintLayers() { + key := htfs.CatalogName(hash) + if exporting && !set.Member(success, key) { + success = append(success, key) + pretty.Note("Added catalog %q to be exported.", key) + } } } } - if len(exportFile) > 0 && len(success) > 0 { + if exporting && len(success) > 0 { holotreeExport(selectCatalogs(success), nil, exportFile) } pretty.Guard(failed == 0, 2, "%d out of %d environment builds failed! See output above for details.", failed, total) diff --git a/common/version.go b/common/version.go index 218afd8e..87f6f582 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.28.0` + Version = `v17.28.1` ) diff --git a/conda/workflows.go b/conda/workflows.go index 51077aa0..66918a85 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -469,8 +469,8 @@ func LogUnifiedEnvironment(content []byte) { common.Log("FINAL unified conda environment descriptor:\n---\n%v---", yaml) } -func finalUnifiedEnvironment(filename string, verbose bool) (string, *Environment, error) { - right, err := ReadCondaYaml(filename) +func finalUnifiedEnvironment(filename string) (string, *Environment, error) { + right, err := ReadPackageCondaYaml(filename) if err != nil { return "", nil, err } @@ -482,7 +482,7 @@ func finalUnifiedEnvironment(filename string, verbose bool) (string, *Environmen } func temporaryConfig(condaYaml, requirementsText, filename string) (string, string, *Environment, error) { - yaml, right, err := finalUnifiedEnvironment(filename, true) + yaml, right, err := finalUnifiedEnvironment(filename) if err != nil { return "", "", nil, err } diff --git a/docs/changelog.md b/docs/changelog.md index 375da890..150fb89b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.28.1 (date: 24.4.2024) + +- bugfix: when exporting prebuild environments, include layer catalogs also +- bugfix: exporting was not adding all environments correctly to .zip file + ## v17.28.0 (date: 22.4.2024) - adding support for `package.yaml` as replacement for `conda.yaml` From c2df2882191708a1ea6dc93b2a8b50438a1d9c5c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Apr 2024 14:48:01 +0300 Subject: [PATCH 497/516] Bugfix: more package/conda related fixes (v17.28.2) - bugfix: more places are now using package/conda YAML loading --- cmd/merge.go | 2 +- common/variables.go | 4 ---- common/version.go | 2 +- conda/condayaml.go | 4 ++-- conda/condayaml_test.go | 10 +++++----- conda/packageyaml.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 2 +- operations/running.go | 2 +- robot/robot.go | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/merge.go b/cmd/merge.go index db81669b..31668e11 100644 --- a/cmd/merge.go +++ b/cmd/merge.go @@ -27,7 +27,7 @@ var mergeCmd = &cobra.Command{ for _, filename := range args { left = right - right, err = conda.ReadCondaYaml(filename) + right, err = conda.ReadPackageCondaYaml(filename) if err != nil { pretty.Exit(1, err.Error()) } diff --git a/common/variables.go b/common/variables.go index 1bc6525a..8766ff7e 100644 --- a/common/variables.go +++ b/common/variables.go @@ -334,10 +334,6 @@ func CaBundleFile() string { return ExpandPath(filepath.Join(RobocorpHome(), "ca-bundle.pem")) } -func CaBundleDir() string { - return ExpandPath(RobocorpHome()) -} - func DefineVerbosity(silent, debug, trace bool) { override := os.Getenv(RCC_VERBOSITY) switch { diff --git a/common/version.go b/common/version.go index 87f6f582..bc8f7f81 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.28.1` + Version = `v17.28.2` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 712c2bf4..e3c0a346 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -163,7 +163,7 @@ func remove(index int, target []*Dependency) []*Dependency { func SummonEnvironment(filename string) *Environment { if pathlib.IsFile(filename) { - result, err := ReadCondaYaml(filename) + result, err := ReadPackageCondaYaml(filename) if err == nil { return result } @@ -658,7 +658,7 @@ func CondaYamlFrom(content []byte) (*Environment, error) { return result.AsEnvironment(), nil } -func ReadCondaYaml(filename string) (*Environment, error) { +func readCondaYaml(filename string) (*Environment, error) { var content []byte var err error diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index fdd00fc4..e7ebebdb 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -74,10 +74,10 @@ func TestCanCreateCondaYamlFromEmptyByteSlice(t *testing.T) { must_be.Equal(0, len(sut.PostInstall)) } -func TestCanReadCondaYaml(t *testing.T) { +func TestCanReadPackageCondaYaml(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - sut, err := conda.ReadCondaYaml("testdata/conda.yaml") + sut, err := conda.ReadPackageCondaYaml("testdata/conda.yaml") must_be.Nil(err) wont_be.Nil(sut) must_be.Equal("", sut.Name) @@ -90,10 +90,10 @@ func TestCanReadCondaYaml(t *testing.T) { func TestCanMergeTwoEnvironments(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - left, err := conda.ReadCondaYaml("testdata/third.yaml") + left, err := conda.ReadPackageCondaYaml("testdata/third.yaml") must_be.Nil(err) wont_be.Nil(left) - right, err := conda.ReadCondaYaml("testdata/other.yaml") + right, err := conda.ReadPackageCondaYaml("testdata/other.yaml") must_be.Nil(err) wont_be.Nil(right) sut, err := left.Merge(right) @@ -122,7 +122,7 @@ func TestCanCreateEmptyEnvironment(t *testing.T) { func TestCanGetLayersFromCondaYaml(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - sut, err := conda.ReadCondaYaml("testdata/layers.yaml") + sut, err := conda.ReadPackageCondaYaml("testdata/layers.yaml") must_be.Nil(err) wont_be.Nil(sut) diff --git a/conda/packageyaml.go b/conda/packageyaml.go index 2a635248..5a44a95d 100644 --- a/conda/packageyaml.go +++ b/conda/packageyaml.go @@ -84,7 +84,7 @@ func ReadPackageCondaYaml(filename string) (*Environment, error) { return environment, nil } } - return ReadCondaYaml(filename) + return readCondaYaml(filename) } func ReadPackageYaml(filename string) (*Environment, error) { diff --git a/docs/changelog.md b/docs/changelog.md index 150fb89b..af75aa58 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.28.2 (date: 24.4.2024) + +- bugfix: more places are now using package/conda YAML loading + ## v17.28.1 (date: 24.4.2024) - bugfix: when exporting prebuild environments, include layer catalogs also diff --git a/htfs/commands.go b/htfs/commands.go index 8b3597c9..2f8c3248 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -212,7 +212,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec } func RestoreLayersTo(tree MutableLibrary, identityfile string, targetDir string) conda.SkipLayer { - config, err := conda.ReadCondaYaml(identityfile) + config, err := conda.ReadPackageCondaYaml(identityfile) if err != nil { return conda.SkipNoLayers } diff --git a/operations/running.go b/operations/running.go index 3e6b3970..624f3012 100644 --- a/operations/running.go +++ b/operations/running.go @@ -102,7 +102,7 @@ func FreezeEnvironmentListing(label string, config robot.Robot) { common.Log("No dependencies found at %q", goldenfile) return } - env, err := conda.ReadCondaYaml(config.CondaConfigFile()) + env, err := conda.ReadPackageCondaYaml(config.CondaConfigFile()) if err != nil { common.Log("Could not read %q, reason: %v", config.CondaConfigFile(), err) return diff --git a/robot/robot.go b/robot/robot.go index 90913e3b..ca41a6fc 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -209,7 +209,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { target.Details["cacheable-environment-configuration"] = "false" } else { diagnose.Ok(0, "In robot.yaml, environment configuration %q is present. So this is python robot.", effectiveConfig) - condaEnv, err := conda.ReadCondaYaml(effectiveConfig) + condaEnv, err := conda.ReadPackageCondaYaml(effectiveConfig) if err != nil { diagnose.Fail(0, "", "From robot.yaml, loading conda.yaml failed with: %v", err) } else { @@ -277,7 +277,7 @@ func (it *robot) VerifyCondaDependencies() bool { if len(dependencies) == 0 { return true } - condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) + condaEnv, err := conda.ReadPackageCondaYaml(it.CondaConfigFile()) if err != nil { return true } From 688a49dd4ae020936f5219b74f377cdfdb39a836 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 26 Apr 2024 10:27:35 +0300 Subject: [PATCH 498/516] Bugfix: made metrics errors not critical (v17.28.3) --- cloud/client.go | 19 +++++++++++++++++-- cloud/metrics.go | 7 ++++--- common/logger.go | 6 ++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ mocks/client.go | 4 ++++ 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index c98bb33c..8ea13144 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -21,6 +21,7 @@ type internalClient struct { endpoint string client *http.Client tracing bool + critical bool } type Request struct { @@ -50,6 +51,7 @@ type Client interface { NewClient(endpoint string) (Client, error) WithTimeout(time.Duration) Client WithTracing() Client + Uncritical() Client } func EnsureHttps(endpoint string) (string, error) { @@ -72,6 +74,7 @@ func NewUnsafeClient(endpoint string) (Client, error) { endpoint: endpoint, client: &http.Client{Transport: settings.Global.ConfiguredHttpTransport()}, tracing: false, + critical: true, }, nil } @@ -84,9 +87,15 @@ func NewClient(endpoint string) (Client, error) { endpoint: https, client: &http.Client{Transport: settings.Global.ConfiguredHttpTransport()}, tracing: false, + critical: true, }, nil } +func (it *internalClient) Uncritical() Client { + it.critical = false + return it +} + func (it *internalClient) WithTimeout(timeout time.Duration) Client { return &internalClient{ endpoint: it.endpoint, @@ -94,7 +103,8 @@ func (it *internalClient) WithTimeout(timeout time.Duration) Client { Transport: settings.Global.ConfiguredHttpTransport(), Timeout: timeout, }, - tracing: it.tracing, + tracing: it.tracing, + critical: it.critical, } } @@ -103,6 +113,7 @@ func (it *internalClient) WithTracing() Client { endpoint: it.endpoint, client: it.client, tracing: true, + critical: it.critical, } } @@ -142,7 +153,11 @@ func (it *internalClient) does(method string, request *Request) *Response { } httpResponse, err := it.client.Do(httpRequest) if err != nil { - common.Error("http.Do", err) + if it.critical { + common.Error("http.Do", err) + } else { + common.Uncritical("http.Do", err) + } response.Status = 9002 response.Err = err return response diff --git a/cloud/metrics.go b/cloud/metrics.go index e62e655f..412a59cb 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -31,13 +31,14 @@ func sendMetric(metricsHost, kind, name, value string) { }() client, err := NewClient(metricsHost) if err != nil { - common.Debug("ERROR: %v", err) + common.Debug("WARNING: %v (not critical)", err) return } - client = client.WithTimeout(5 * time.Second) + timeout := 5 * time.Second + client = client.Uncritical().WithTimeout(timeout) timestamp := time.Now().UnixNano() url := fmt.Sprintf(trackingUrl, url.PathEscape(kind), timestamp, url.PathEscape(xviper.TrackingIdentity()), url.PathEscape(name), url.PathEscape(value)) - common.Debug("Sending metric as %v%v", metricsHost, url) + common.Debug("Sending metric (timeout %v) as %v%v", timeout, metricsHost, url) client.Put(client.NewRequest(url)) } diff --git a/common/logger.go b/common/logger.go index 20739b33..1b94e0f4 100644 --- a/common/logger.go +++ b/common/logger.go @@ -75,6 +75,12 @@ func Error(context string, err error) { } } +func Uncritical(context string, err error) { + if err != nil { + Log("Warning [%s; not critical]: %v", context, err) + } +} + func Log(format string, details ...interface{}) { if !Silent() { prefix := "" diff --git a/common/version.go b/common/version.go index bc8f7f81..72522741 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.28.2` + Version = `v17.28.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index af75aa58..1206dc81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.28.3 (date: 26.4.2024) + +- bugfix: metrics sending was stating things as error, but they are not + critical (so that is now mentioned in message) + ## v17.28.2 (date: 24.4.2024) - bugfix: more places are now using package/conda YAML loading diff --git a/mocks/client.go b/mocks/client.go index 4739d26c..a07858d3 100644 --- a/mocks/client.go +++ b/mocks/client.go @@ -29,6 +29,10 @@ func (it *MockClient) NewClient(endpoint string) (cloud.Client, error) { return it, nil } +func (it *MockClient) Uncritical() cloud.Client { + return it +} + func (it *MockClient) WithTimeout(time.Duration) cloud.Client { return it } From a79fb06a2ce4469692acf1535cf737022b85f1a1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 26 Apr 2024 13:50:02 +0300 Subject: [PATCH 499/516] Bugfix: missing controller from message (v17.28.4) - bugfix: when there is "rcc point of view" message, it was not showing who was controller, so now controller is visible --- common/version.go | 2 +- docs/changelog.md | 5 +++++ pretty/functions.go | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 72522741..211c8cf5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.28.3` + Version = `v17.28.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1206dc81..02abc042 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.28.4 (date: 26.4.2024) + +- bugfix: when there is "rcc point of view" message, it was not showing + who was controller, so now controller is visible + ## v17.28.3 (date: 26.4.2024) - bugfix: metrics sending was stating things as error, but they are not diff --git a/pretty/functions.go b/pretty/functions.go index 47b39351..f1229fdc 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -9,7 +9,7 @@ import ( ) const ( - rccpov = `From rcc %q point of view, %q was` + rccpov = `From rcc %q (controller: %q) point of view, %q was` maxSteps = 15 ) @@ -78,7 +78,7 @@ func Guard(truth bool, code int, format string, rest ...interface{}) { } func RccPointOfView(context string, err error) { - explain := fmt.Sprintf(rccpov, common.Version, context) + explain := fmt.Sprintf(rccpov, common.Version, common.ControllerType, context) printer := Lowlight message := fmt.Sprintf("@@@ %s SUCCESS. @@@", explain) journal := fmt.Sprintf("%s SUCCESS.", explain) From a43b3dcfc5723dba3de6d3f4d068e6bfd8cc71a0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 27 May 2024 13:14:59 +0300 Subject: [PATCH 500/516] Bugfix: removing VIRTUAL_ENV from subprocesses (v17.29.0) --- common/version.go | 2 +- conda/robocorp.go | 18 ++++++++++++++++++ docs/changelog.md | 5 +++++ operations/diagnostics.go | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 211c8cf5..1ae8dee0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.28.4` + Version = `v17.29.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index bd5d109b..ac069a49 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -13,6 +13,7 @@ import ( "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" "github.com/robocorp/rcc/xviper" @@ -137,10 +138,27 @@ func injectNetworkEnvironment(environment []string) []string { return environment } +func removeIncompatibleEnvironmentVariables(environment []string, unwanted ...string) []string { + result := make([]string, 0, len(environment)) +search: + for _, here := range environment { + parts := strings.Split(strings.TrimSpace(here), "=") + for _, name := range unwanted { + if strings.EqualFold(name, parts[0]) { + pretty.Warning("Removing incompatible variable %q from environment.", here) + continue search + } + } + result = append(result, here) + } + return result +} + func CondaExecutionEnvironment(location string, inject []string, full bool) []string { environment := make([]string, 0, 100) if full { environment = append(environment, os.Environ()...) + environment = removeIncompatibleEnvironmentVariables(environment, "VIRTUAL_ENV") } if inject != nil && len(inject) > 0 { environment = append(environment, inject...) diff --git a/docs/changelog.md b/docs/changelog.md index 02abc042..bcc81b04 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.29.0 (date: 27.5.2024) + +- bugfix: removing `VIRTUAL_ENV` when rcc is executing subprocesses +- adding warning about that environment variable also in diagnostics + ## v17.28.4 (date: 26.4.2024) - bugfix: when there is "rcc point of view" message, it was not showing diff --git a/operations/diagnostics.go b/operations/diagnostics.go index f24d64b0..64178675 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -155,6 +155,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Checks = append(result.Checks, anyPathCheck("SSL_CERT_DIR")) result.Checks = append(result.Checks, anyPathCheck("SSL_CERT_FILE")) result.Checks = append(result.Checks, anyPathCheck("WDM_SSL_VERIFY")) + result.Checks = append(result.Checks, anyPathCheck("VIRTUAL_ENV")) result.Checks = append(result.Checks, anyEnvVarCheck("RCC_NO_TEMP_MANAGEMENT")) result.Checks = append(result.Checks, anyEnvVarCheck("RCC_NO_PYC_MANAGEMENT")) From 73561cfe22121d1fff461e29fc9464102ee14d8f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 29 May 2024 12:00:50 +0300 Subject: [PATCH 501/516] Bugfix: locking making dirs public (v17.29.1) - bugfix: when taking locks, some of those need to be in shared directory, while others should not; code was making too much directories shared --- common/version.go | 2 +- conda/cleanup.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 2 +- htfs/library.go | 2 +- htfs/virtual.go | 2 +- htfs/ziplibrary.go | 2 +- operations/cache.go | 4 ++-- operations/pull.go | 2 +- pathlib/lock_unix.go | 15 +++++++++++---- pathlib/lock_windows.go | 15 +++++++++++---- xviper/wrapper.go | 4 ++-- 13 files changed, 39 insertions(+), 20 deletions(-) diff --git a/common/version.go b/common/version.go index 1ae8dee0..43034522 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.29.0` + Version = `v17.29.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 7a9dfd62..791a0b63 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -180,7 +180,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress lockfile := common.RobocorpLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment cleanup [robocorp lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, false) completed() if err != nil { common.Log("Could not get lock on live environment. Quitting!") diff --git a/conda/workflows.go b/conda/workflows.go index 66918a85..9f1dd10c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -501,7 +501,7 @@ func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configurat lockfile := common.RobocorpLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [robocorp lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, false) completed() if err != nil { common.Log("Could not get lock on live environment. Quitting!") diff --git a/docs/changelog.md b/docs/changelog.md index bcc81b04..74696670 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.29.1 (date: 29.5.2024) + +- bugfix: when taking locks, some of those need to be in shared directory, + while others should not; code was making too much directories shared + ## v17.29.0 (date: 27.5.2024) - bugfix: removing `VIRTUAL_ENV` when rcc is executing subprocesses diff --git a/htfs/commands.go b/htfs/commands.go index 2f8c3248..a42be109 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -79,7 +79,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal lockfile := common.HolotreeLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [holotree lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, common.SharedHolotree) completed() fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/htfs/library.go b/htfs/library.go index 6fe95cab..f4265986 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -395,7 +395,7 @@ func (it *hololib) RestoreTo(blueprint []byte, label, controller, space string, metafile := fmt.Sprintf("%s.meta", targetdir) lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, common.SharedHolotree) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() diff --git a/htfs/virtual.go b/htfs/virtual.go index 334adf45..c9c9d077 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -103,7 +103,7 @@ func (it *virtual) RestoreTo(blueprint []byte, label, controller, space string, metafile := fmt.Sprintf("%s.meta", targetdir) lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree virtual lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, common.SharedHolotree) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 758582a6..fa56ec0b 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -120,7 +120,7 @@ func (it *ziplibrary) RestoreTo(blueprint []byte, label, controller, space strin metafile := fmt.Sprintf("%s.meta", targetdir) lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, common.SharedHolotree) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() diff --git a/operations/cache.go b/operations/cache.go index 9e007dc4..89168ef7 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -70,7 +70,7 @@ func SummonCache() (*Cache, error) { var result Cache lockfile := cacheLockFile() completed := pathlib.LockWaitMessage(lockfile, "Serialized cache access [cache lock]") - locker, err := pathlib.Locker(lockfile, 125) + locker, err := pathlib.Locker(lockfile, 125, false) completed() if err != nil { return nil, err @@ -98,7 +98,7 @@ func (it *Cache) Save() error { } lockfile := cacheLockFile() completed := pathlib.LockWaitMessage(lockfile, "Serialized cache access [cache lock]") - locker, err := pathlib.Locker(lockfile, 125) + locker, err := pathlib.Locker(lockfile, 125, false) completed() if err != nil { return err diff --git a/operations/pull.go b/operations/pull.go index 34a5d232..97656895 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -129,7 +129,7 @@ func ProtectedImport(filename string) (err error) { lockfile := common.HolotreeLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment import [holotree lock]") - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(lockfile, 30000, common.SharedHolotree) completed() fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index f623481f..90272be0 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -10,7 +10,7 @@ import ( "github.com/robocorp/rcc/common" ) -func Locker(filename string, trycount int) (Releaser, error) { +func Locker(filename string, trycount int, sharedLocation bool) (Releaser, error) { if common.WarrantyVoided() || Lockless { return Fake(), nil } @@ -18,9 +18,16 @@ func Locker(filename string, trycount int) (Releaser, error) { defer common.Stopwatch("LOCKER: Got lock on %v in", filename).Report() } common.Trace("LOCKER: Want lock on: %v", filename) - _, err := EnsureSharedParentDirectory(filename) - if err != nil { - return nil, err + if sharedLocation { + _, err := EnsureSharedParentDirectory(filename) + if err != nil { + return nil, err + } + } else { + _, err := EnsureParentDirectory(filename) + if err != nil { + return nil, err + } } file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) if err != nil { diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 6027068b..a101dce7 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -28,7 +28,7 @@ type filehandle interface { Fd() uintptr } -func Locker(filename string, trycount int) (Releaser, error) { +func Locker(filename string, trycount int, sharedLocation bool) (Releaser, error) { if common.WarrantyVoided() || Lockless { return Fake(), nil } @@ -40,9 +40,16 @@ func Locker(filename string, trycount int) (Releaser, error) { }() } common.Trace("LOCKER: Want lock on: %v", filename) - _, err = EnsureSharedParentDirectory(filename) - if err != nil { - return nil, err + if sharedLocation { + _, err = EnsureSharedParentDirectory(filename) + if err != nil { + return nil, err + } + } else { + _, err := EnsureParentDirectory(filename) + if err != nil { + return nil, err + } } for { trycount -= 1 diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 55e95123..f536ba14 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -42,7 +42,7 @@ func (it *config) Save() { return } completed := pathlib.LockWaitMessage(it.Lockfile, "Serialized config access [config lock]") - locker, err := pathlib.Locker(it.Lockfile, 125) + locker, err := pathlib.Locker(it.Lockfile, 125, false) completed() if err != nil { common.Log("FATAL: could not lock %v, reason %v; ignored.", it.Lockfile, err) @@ -64,7 +64,7 @@ func (it *config) Save() { func (it *config) reload() { completed := pathlib.LockWaitMessage(it.Lockfile, "Serialized config access [config lock]") - locker, err := pathlib.Locker(it.Lockfile, 125) + locker, err := pathlib.Locker(it.Lockfile, 125, false) completed() if err != nil { common.Log("FATAL: could not lock %v, reason %v; ignored.", it.Lockfile, err) From ede14c0a079fe0aa453fd8f2eddb97daeaa53e30 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 3 Jun 2024 11:42:19 +0300 Subject: [PATCH 502/516] Feature: sema4ai flag (v18.0.0) - MAJOR breaking change: rcc will now live in two product domains, Robocorp and Sema4.ai - feature: initial support for `--sema4ai` strategy selection - robot tests to test Sema4.ai support --- README.md | 2 +- cmd/assistant.go | 7 ++- cmd/cloudNew.go | 14 +++-- cmd/cloudPrepare.go | 14 +++-- cmd/command_linux.go | 6 +- cmd/command_windows.go | 2 +- cmd/community.go | 5 +- cmd/communitypull.go | 10 ++-- cmd/configureprofile.go | 4 +- cmd/download.go | 14 +++-- cmd/events.go | 5 +- cmd/interactive.go | 5 +- cmd/internal.go | 7 ++- cmd/man.go | 7 ++- cmd/pull.go | 18 +++--- cmd/push.go | 16 ++--- cmd/rcc/main.go | 2 +- cmd/robot.go | 5 +- cmd/root.go | 14 ++++- cmd/run.go | 30 +++++----- cmd/speed.go | 6 +- cmd/task.go | 6 +- cmd/upload.go | 14 +++-- cmd/wizardcreate.go | 6 +- common/categories.go | 34 +++++------ common/diagnostics.go | 6 +- common/platform_darwin.go | 3 + common/platform_linux.go | 3 + common/platform_windows.go | 3 + common/strategies.go | 88 ++++++++++++++++++++++++++++ common/variables.go | 71 ++++++++++------------ common/version.go | 2 +- conda/activate.go | 2 +- conda/cleanup.go | 8 +-- conda/platform_windows.go | 2 +- conda/robocorp.go | 4 +- conda/validate.go | 3 +- conda/workflows.go | 2 +- docs/changelog.md | 7 +++ htfs/directory.go | 2 +- htfs/library.go | 2 +- htfs/virtual.go | 2 +- operations/cache.go | 2 +- operations/diagnostics.go | 38 ++++++------ operations/running.go | 6 +- robot/robot.go | 4 +- robot_tests/bare_action/package.yaml | 14 +++++ robot_tests/fullrun.robot | 2 +- robot_tests/profiles.robot | 1 - robot_tests/resources.robot | 11 +++- robot_tests/sema4ai.robot | 51 ++++++++++++++++ settings/profile.go | 2 +- 52 files changed, 404 insertions(+), 190 deletions(-) create mode 100644 common/strategies.go create mode 100644 robot_tests/bare_action/package.yaml create mode 100644 robot_tests/sema4ai.robot diff --git a/README.md b/README.md index dcc929d0..a284177f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Together with [robot.yaml](https://robocorp.com/docs/robot-structure/robot-yaml-

-RCC is actively maintained by [Robocorp](https://www.robocorp.com/). +RCC is actively maintained by [Sema4.ai](https://sema4.ai/). ## Why use rcc? diff --git a/cmd/assistant.go b/cmd/assistant.go index 6f8423d3..a1a10b4d 100644 --- a/cmd/assistant.go +++ b/cmd/assistant.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -13,7 +14,9 @@ They are either local, or in relation to Robocorp Control Room and tooling.`, } func init() { - rootCmd.AddCommand(assistantCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(assistantCmd) - assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", "Account used for Robocorp Control Room operations.") + assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", "Account used for Robocorp Control Room operations.") + } } diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index dace06a0..03ed1f77 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -44,10 +44,12 @@ var newCloudCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(newCloudCmd) - newCloudCmd.Flags().StringVarP(&robotName, "robot", "r", "", "Name for new robot to create.") - newCloudCmd.MarkFlagRequired("robot") - newCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use as creation target.") - newCloudCmd.MarkFlagRequired("workspace") - newCloudCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(newCloudCmd) + newCloudCmd.Flags().StringVarP(&robotName, "robot", "r", "", "Name for new robot to create.") + newCloudCmd.MarkFlagRequired("robot") + newCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use as creation target.") + newCloudCmd.MarkFlagRequired("workspace") + newCloudCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + } } diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 4737aa28..7ff0761d 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -64,10 +64,12 @@ var prepareCloudCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(prepareCloudCmd) - prepareCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - prepareCloudCmd.MarkFlagRequired("workspace") - prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - prepareCloudCmd.MarkFlagRequired("robot") - prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(prepareCloudCmd) + prepareCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("workspace") + prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("robot") + prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") + } } diff --git a/cmd/command_linux.go b/cmd/command_linux.go index 5bbaa39e..684969b6 100644 --- a/cmd/command_linux.go +++ b/cmd/command_linux.go @@ -14,11 +14,11 @@ func osSpecificHolotreeSharing(enable bool) { return } pathlib.ForceShared() - parent := filepath.Dir(common.HoloLocation()) + parent := filepath.Dir(common.Product.HoloLocation()) _, err := pathlib.ForceSharedDir(parent) pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) - _, err = pathlib.ForceSharedDir(common.HoloLocation()) - pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.HoloLocation(), err) + _, err = pathlib.ForceSharedDir(common.Product.HoloLocation()) + pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.Product.HoloLocation(), err) err = os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) pretty.Guard(err == nil, 3, "Could not write %q, reason: %v", common.SharedMarkerLocation(), err) } diff --git a/cmd/command_windows.go b/cmd/command_windows.go index 4b2f3d70..6cce83f4 100644 --- a/cmd/command_windows.go +++ b/cmd/command_windows.go @@ -15,7 +15,7 @@ func osSpecificHolotreeSharing(enable bool) { return } pathlib.ForceShared() - parent := filepath.Dir(common.HoloLocation()) + parent := filepath.Dir(common.Product.HoloLocation()) _, err := pathlib.ForceSharedDir(parent) pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) task := shell.New(nil, ".", "icacls", "C:/ProgramData/robocorp", "/grant", "*S-1-5-32-545:(OI)(CI)M", "/T", "/Q") diff --git a/cmd/community.go b/cmd/community.go index e6e34c1d..402d21e2 100644 --- a/cmd/community.go +++ b/cmd/community.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -12,5 +13,7 @@ var communityCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(communityCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(communityCmd) + } } diff --git a/cmd/communitypull.go b/cmd/communitypull.go index 1f781fbd..4e534952 100644 --- a/cmd/communitypull.go +++ b/cmd/communitypull.go @@ -56,8 +56,10 @@ var communityPullCmd = &cobra.Command{ } func init() { - communityCmd.AddCommand(communityPullCmd) - rootCmd.AddCommand(communityPullCmd) - communityPullCmd.Flags().StringVarP(&branch, "branch", "b", "main", "Branch/tag/commitid to use as basis for robot.") - communityPullCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to extract the robot into.") + if common.Product.IsLegacy() { + communityCmd.AddCommand(communityPullCmd) + rootCmd.AddCommand(communityPullCmd) + communityPullCmd.Flags().StringVarP(&branch, "branch", "b", "main", "Branch/tag/commitid to use as basis for robot.") + communityPullCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to extract the robot into.") + } } diff --git a/cmd/configureprofile.go b/cmd/configureprofile.go index c9960ceb..fc2bd8e2 100644 --- a/cmd/configureprofile.go +++ b/cmd/configureprofile.go @@ -22,7 +22,7 @@ var ( ) func profileMap() map[string]string { - pattern := common.ExpandPath(filepath.Join(common.RobocorpHome(), "profile_*.yaml")) + pattern := common.ExpandPath(filepath.Join(common.Product.Home(), "profile_*.yaml")) found, err := filepath.Glob(pattern) pretty.Guard(err == nil, 1, "Error while searching profiles: %v", err) profiles := make(map[string]string) @@ -57,7 +57,7 @@ func listProfiles() { func profileFullPath(name string) string { filename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) - return common.ExpandPath(filepath.Join(common.RobocorpHome(), filename)) + return common.ExpandPath(filepath.Join(common.Product.Home(), filename)) } func loadNamedProfile(name string) *settings.Profile { diff --git a/cmd/download.go b/cmd/download.go index abff15ab..b7b50f64 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -34,10 +34,12 @@ var downloadCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(downloadCmd) - downloadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the downloaded robot.") - downloadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - downloadCmd.MarkFlagRequired("workspace") - downloadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - downloadCmd.MarkFlagRequired("robot") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(downloadCmd) + downloadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the downloaded robot.") + downloadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + downloadCmd.MarkFlagRequired("workspace") + downloadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + downloadCmd.MarkFlagRequired("robot") + } } diff --git a/cmd/events.go b/cmd/events.go index c3b95e39..b8a25c46 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -6,6 +6,7 @@ import ( "os" "text/tabwriter" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -24,8 +25,8 @@ func humaneEventListing(events []journal.Event) { var eventsCmd = &cobra.Command{ Use: "events", - Short: "Show events from event journal (ROBOCORP_HOME/event.log).", - Long: "Show events from event journal (ROBOCORP_HOME/event.log).", + Short: fmt.Sprintf("Show events from event journal (%s/event.log).", common.Product.HomeVariable()), + Long: fmt.Sprintf("Show events from event journal (%s/event.log).", common.Product.HomeVariable()), Run: func(cmd *cobra.Command, args []string) { events, err := journal.Events() pretty.Guard(err == nil, 2, "Error while loading events: %v", err) diff --git a/cmd/interactive.go b/cmd/interactive.go index 0b4cd425..60b7fa80 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -13,5 +14,7 @@ Do not try to use these in automation, they will fail there.`, } func init() { - rootCmd.AddCommand(interactiveCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(interactiveCmd) + } } diff --git a/cmd/internal.go b/cmd/internal.go index 3faa6b3e..a93861cf 100644 --- a/cmd/internal.go +++ b/cmd/internal.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -13,6 +14,8 @@ var internalCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(internalCmd) - internalCmd.PersistentFlags().StringVarP(&wskey, "wskey", "", "", "Cloud API workspace key (authorization).") + if common.Product.IsLegacy() { + rootCmd.AddCommand(internalCmd) + internalCmd.PersistentFlags().StringVarP(&wskey, "wskey", "", "", "Cloud API workspace key (authorization).") + } } diff --git a/cmd/man.go b/cmd/man.go index 06eef1d6..58956b66 100644 --- a/cmd/man.go +++ b/cmd/man.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -101,8 +102,10 @@ func init() { Run: makeShowDoc("vocabulary documentation", "docs/vocabulary.md"), }) - manCmd.AddCommand(tutorial) - rootCmd.AddCommand(tutorial) + if common.Product.IsLegacy() { + manCmd.AddCommand(tutorial) + rootCmd.AddCommand(tutorial) + } } func makeShowDoc(label, asset string) cobraCommand { diff --git a/cmd/pull.go b/cmd/pull.go index 6978aca2..6abeb5fe 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -52,12 +52,14 @@ var pullCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(pullCmd) - pullCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - pullCmd.MarkFlagRequired("workspace") - pullCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - pullCmd.MarkFlagRequired("robot") - pullCmd.Flags().StringVarP(&directory, "directory", "d", "", "The root directory to extract the robot into.") - pullCmd.MarkFlagRequired("directory") - pullCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Remove safety nets around the unwrapping of the robot.") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(pullCmd) + pullCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + pullCmd.MarkFlagRequired("workspace") + pullCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + pullCmd.MarkFlagRequired("robot") + pullCmd.Flags().StringVarP(&directory, "directory", "d", "", "The root directory to extract the robot into.") + pullCmd.MarkFlagRequired("directory") + pullCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Remove safety nets around the unwrapping of the robot.") + } } diff --git a/cmd/push.go b/cmd/push.go index 646709e0..7606ebcf 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -49,11 +49,13 @@ var pushCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(pushCmd) - pushCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to create the robot from.") - pushCmd.Flags().StringArrayVarP(&ignores, "ignore", "i", []string{}, "Files containing ignore patterns.") - pushCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") - pushCmd.MarkFlagRequired("workspace") - pushCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") - pushCmd.MarkFlagRequired("robot") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(pushCmd) + pushCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to create the robot from.") + pushCmd.Flags().StringArrayVarP(&ignores, "ignore", "i", []string{}, "Files containing ignore patterns.") + pushCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") + pushCmd.MarkFlagRequired("workspace") + pushCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") + pushCmd.MarkFlagRequired("robot") + } } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index dcb2a595..db61331a 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -44,7 +44,7 @@ func EnsureUserRegistered() (string, error) { updated, ok := set.Update(cache.Users, strings.ToLower(who.Username)) size := len(updated) if size > 1 { - warning = fmt.Sprintf("More than one user is using same ROBOCORP_HOME location! Those users are: %s!", strings.Join(updated, ", ")) + warning = fmt.Sprintf("More than one user is using same %s location! Those users are: %s!", common.Product.HomeVariable(), strings.Join(updated, ", ")) } if !ok { return warning, nil diff --git a/cmd/robot.go b/cmd/robot.go index 69d6c5c6..cdd7b487 100644 --- a/cmd/robot.go +++ b/cmd/robot.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -13,5 +14,7 @@ executed either locally, or in connection to Robocorp Control Room and tooling.` } func init() { - rootCmd.AddCommand(robotCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(robotCmd) + } } diff --git a/cmd/root.go b/cmd/root.go index b425d0f6..524b7626 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" @@ -25,6 +26,9 @@ var ( silentFlag bool debugFlag bool traceFlag bool + sema4FakeFlag bool + + excludedCommands = []string{"completion"} ) func toplevelCommands(parent *cobra.Command) { @@ -45,10 +49,13 @@ func commandTree(level int, prefix string, parent *cobra.Command) { if level == 1 && len(parent.Commands()) == 0 { return } + name := strings.Split(parent.Use, " ") + if set.Member(excludedCommands, name[0]) { + return + } if level == 1 { common.Log("%s", strings.TrimSpace(prefix)) } - name := strings.Split(parent.Use, " ") label := fmt.Sprintf("%s%s", prefix, name[0]) common.Log("%-16s %s", label, parent.Short) indent := prefix + "| " @@ -108,9 +115,10 @@ func init() { rootCmd.PersistentFlags().StringVar(&profilefile, "pprof", "", "Filename to save profiling information.") rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("config file (default is $%s/rcc.yaml)", common.Product.HomeVariable())) rootCmd.PersistentFlags().StringVar(&anythingIgnore, "anything", "", "freeform string value that can be set without any effect, for example CLI versioning/reference") + rootCmd.PersistentFlags().BoolVarP(&sema4FakeFlag, "sema4ai", "", false, "Select Sema4.ai toolset strategy.") rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib (also RCC_NO_BUILD=1)") rootCmd.PersistentFlags().BoolVarP(&common.NoRetryBuild, "no-retry-build", "", false, "no retry in case of first environment build fails, just report error immediately") rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output (also RCC_VERBOSITY=silent)") @@ -145,7 +153,7 @@ func initConfig() { if cfgFile != "" { xviper.SetConfigFile(cfgFile) } else { - xviper.SetConfigFile(filepath.Join(common.RobocorpHome(), "rcc.yaml")) + xviper.SetConfigFile(filepath.Join(common.Product.Home(), "rcc.yaml")) } common.DefineVerbosity(silentFlag, debugFlag, traceFlag) diff --git a/cmd/run.go b/cmd/run.go index dad1daa0..567156c6 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -52,19 +52,21 @@ func captureRunFlags(assistant bool) *operations.RunFlags { } func init() { - rootCmd.AddCommand(runCmd) - taskCmd.AddCommand(runCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(runCmd) + taskCmd.AddCommand(runCmd) - runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") - runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") - runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") - runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") - runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") - runCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") - runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") - runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") - runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") - runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") - runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") - runCmd.Flags().BoolVarP(&common.DeveloperFlag, "dev", "", false, "Use devTasks instead of normal tasks. For development work only. Stragegy selection.") + runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") + runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") + runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") + runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") + runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + runCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") + runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") + runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") + runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") + runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") + runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") + runCmd.Flags().BoolVarP(&common.DeveloperFlag, "dev", "", false, "Use devTasks instead of normal tasks. For development work only. Stragegy selection.") + } } diff --git a/cmd/speed.go b/cmd/speed.go index ba11b4df..f1b426ff 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -70,7 +70,7 @@ var speedtestCmd = &cobra.Command{ } go workingWorm(signal, timing, debug) folder := common.RobocorpTemp() - pretty.DebugNote("Speed test will force temporary ROBOCORP_HOME to be %q while testing.", folder) + pretty.DebugNote("Speed test will force temporary %s to be %q while testing.", common.Product.HomeVariable(), folder) err := os.RemoveAll(folder) pretty.Guard(err == nil, 4, "Error: %v", err) content, err := blobs.Asset("assets/speedtest.yaml") @@ -78,11 +78,11 @@ var speedtestCmd = &cobra.Command{ condafile := filepath.Join(folder, "speedtest.yaml") err = pathlib.WriteFile(condafile, content, 0o666) pretty.Guard(err == nil, 2, "Error: %v", err) - common.ForcedRobocorpHome = folder + common.Product.ForceHome(folder) _, score, err := htfs.NewEnvironment(condafile, "", true, true, operations.PullCatalog) common.DefineVerbosity(silent, debug, trace) pretty.Guard(err == nil, 3, "Error: %v", err) - common.ForcedRobocorpHome = "" + common.Product.ForceHome("") err = os.RemoveAll(folder) pretty.Guard(err == nil, 4, "Error: %v", err) score.Done() diff --git a/cmd/task.go b/cmd/task.go index 8bdda6de..49a45469 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -14,7 +14,9 @@ executed either locally, or in connection to Robocorp Control Room and tooling.` } func init() { - rootCmd.AddCommand(taskCmd) + if common.Product.IsLegacy() { + rootCmd.AddCommand(taskCmd) - taskCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") + taskCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") + } } diff --git a/cmd/upload.go b/cmd/upload.go index e94b47af..5eaa2701 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -34,10 +34,12 @@ var uploadCmd = &cobra.Command{ } func init() { - cloudCmd.AddCommand(uploadCmd) - uploadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the robot.") - uploadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") - uploadCmd.MarkFlagRequired("workspace") - uploadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") - uploadCmd.MarkFlagRequired("robot") + if common.Product.IsLegacy() { + cloudCmd.AddCommand(uploadCmd) + uploadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the robot.") + uploadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") + uploadCmd.MarkFlagRequired("workspace") + uploadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") + uploadCmd.MarkFlagRequired("robot") + } } diff --git a/cmd/wizardcreate.go b/cmd/wizardcreate.go index ea918632..7926eb60 100644 --- a/cmd/wizardcreate.go +++ b/cmd/wizardcreate.go @@ -27,6 +27,8 @@ var wizardCreateCmd = &cobra.Command{ } func init() { - interactiveCmd.AddCommand(wizardCreateCmd) - rootCmd.AddCommand(wizardCreateCmd) + if common.Product.IsLegacy() { + interactiveCmd.AddCommand(wizardCreateCmd) + rootCmd.AddCommand(wizardCreateCmd) + } } diff --git a/common/categories.go b/common/categories.go index a07ac0a3..b2f540c9 100644 --- a/common/categories.go +++ b/common/categories.go @@ -1,21 +1,21 @@ package common const ( - CategoryUndefined = 0 - CategoryLongPath = 1010 - CategoryLockFile = 1020 - CategoryLockPid = 1021 - CategoryPathCheck = 1030 - CategoryEnvVarCheck = 1040 - CategoryHolotreeShared = 2010 - CategoryRobocorpHome = 3010 - CategoryRobocorpHomeMembers = 3020 - CategoryNetworkDNS = 4010 - CategoryNetworkLink = 4020 - CategoryNetworkHEAD = 4030 - CategoryNetworkCanary = 4040 - CategoryNetworkTLSVersion = 4050 - CategoryNetworkTLSVerify = 4060 - CategoryNetworkTLSChain = 4070 - CategoryEnvironmentCache = 5010 + CategoryUndefined = 0 + CategoryLongPath = 1010 + CategoryLockFile = 1020 + CategoryLockPid = 1021 + CategoryPathCheck = 1030 + CategoryEnvVarCheck = 1040 + CategoryHolotreeShared = 2010 + CategoryProductHome = 3010 + CategoryProductHomeMembers = 3020 + CategoryNetworkDNS = 4010 + CategoryNetworkLink = 4020 + CategoryNetworkHEAD = 4030 + CategoryNetworkCanary = 4040 + CategoryNetworkTLSVersion = 4050 + CategoryNetworkTLSVerify = 4060 + CategoryNetworkTLSChain = 4070 + CategoryEnvironmentCache = 5010 ) diff --git a/common/diagnostics.go b/common/diagnostics.go index d26e9ea0..58c836d5 100644 --- a/common/diagnostics.go +++ b/common/diagnostics.go @@ -78,14 +78,14 @@ func (it *DiagnosticStatus) AsJson() (string, error) { return string(body), nil } -func IsInsideRobocorpHome(location string) (_ bool, err error) { +func IsInsideProductHome(location string) (_ bool, err error) { defer fail.Around(&err) candidate, err := filepath.Abs(location) fail.On(err != nil, "Failed to get absolute path to %q, reason: %v", location, err) - rchome, err := filepath.Abs(RobocorpHome()) - fail.On(err != nil, "Failed to get absolute path to ROBOCORP_HOME, reason: %v", err) + rchome, err := filepath.Abs(Product.Home()) + fail.On(err != nil, "Failed to get absolute path to %s, reason: %v", Product.HomeVariable(), err) for len(rchome) <= len(candidate) { if rchome == candidate { diff --git a/common/platform_darwin.go b/common/platform_darwin.go index 0e2bb2b3..c7b7dafe 100644 --- a/common/platform_darwin.go +++ b/common/platform_darwin.go @@ -11,6 +11,9 @@ import ( const ( defaultRobocorpLocation = "$HOME/.robocorp" defaultHoloLocation = "/Users/Shared/robocorp/ht" + + defaultSema4Location = "$HOME/.sema4ai" + defaultSema4HoloLocation = "/Users/Shared/sema4ai/ht" ) func ExpandPath(entry string) string { diff --git a/common/platform_linux.go b/common/platform_linux.go index 392a8cc1..77527389 100644 --- a/common/platform_linux.go +++ b/common/platform_linux.go @@ -11,6 +11,9 @@ import ( const ( defaultRobocorpLocation = "$HOME/.robocorp" defaultHoloLocation = "/opt/robocorp/ht" + + defaultSema4Location = "$HOME/.sema4ai" + defaultSema4HoloLocation = "/opt/sema4ai/ht" ) func ExpandPath(entry string) string { diff --git a/common/platform_windows.go b/common/platform_windows.go index ef5d856d..05977132 100644 --- a/common/platform_windows.go +++ b/common/platform_windows.go @@ -12,6 +12,9 @@ import ( const ( defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" defaultHoloLocation = "c:\\ProgramData\\robocorp\\ht" + + defaultSema4Location = "%LOCALAPPDATA%\\sema4ai" + defaultSema4HoloLocation = "c:\\ProgramData\\sema4ai\\ht" ) var ( diff --git a/common/strategies.go b/common/strategies.go new file mode 100644 index 00000000..02b283d8 --- /dev/null +++ b/common/strategies.go @@ -0,0 +1,88 @@ +package common + +import "os" + +const ( + ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + SEMA4AI_HOME_VARIABLE = `SEMA4AI_HOME` +) + +type ( + ProductStrategy interface { + IsLegacy() bool + ForceHome(string) + HomeVariable() string + Home() string + HoloLocation() string + } + + legacyStrategy struct { + forcedHome string + } + + sema4Strategy struct { + forcedHome string + } +) + +func LegacyMode() ProductStrategy { + return &legacyStrategy{} +} + +func Sema4Mode() ProductStrategy { + return &sema4Strategy{} +} + +func (it *legacyStrategy) IsLegacy() bool { + return true +} + +func (it *legacyStrategy) ForceHome(value string) { + it.forcedHome = value +} + +func (it *legacyStrategy) HomeVariable() string { + return ROBOCORP_HOME_VARIABLE +} + +func (it *legacyStrategy) Home() string { + if len(it.forcedHome) > 0 { + return ExpandPath(it.forcedHome) + } + home := os.Getenv(it.HomeVariable()) + if len(home) > 0 { + return ExpandPath(home) + } + return ExpandPath(defaultRobocorpLocation) +} + +func (it *legacyStrategy) HoloLocation() string { + return ExpandPath(defaultHoloLocation) +} + +func (it *sema4Strategy) IsLegacy() bool { + return false +} + +func (it *sema4Strategy) ForceHome(value string) { + it.forcedHome = value +} + +func (it *sema4Strategy) HomeVariable() string { + return SEMA4AI_HOME_VARIABLE +} + +func (it *sema4Strategy) Home() string { + if len(it.forcedHome) > 0 { + return ExpandPath(it.forcedHome) + } + home := os.Getenv(it.HomeVariable()) + if len(home) > 0 { + return ExpandPath(home) + } + return ExpandPath(defaultSema4Location) +} + +func (it *sema4Strategy) HoloLocation() string { + return ExpandPath(defaultSema4HoloLocation) +} diff --git a/common/variables.go b/common/variables.go index 8766ff7e..0a19dc23 100644 --- a/common/variables.go +++ b/common/variables.go @@ -25,7 +25,6 @@ const ( ) const ( - ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` RCC_REMOTE_ORIGIN = `RCC_REMOTE_ORIGIN` RCC_REMOTE_AUTHORIZATION = `RCC_REMOTE_AUTHORIZATION` RCC_NO_TEMP_MANAGEMENT = `RCC_NO_TEMP_MANAGEMENT` @@ -60,12 +59,12 @@ var ( HolotreeSpace string EnvironmentHash string SemanticTag string - ForcedRobocorpHome string When int64 Clock *stopwatch randomIdentifier string verbosity Verbosity LogHides []string + Product ProductStrategy ) func init() { @@ -82,6 +81,11 @@ func init() { args := set.Set(lowargs) WarrantyVoidedFlag = set.Member(args, "--warranty-voided") BundledFlag = set.Member(args, "--bundled") + if set.Member(args, "--sema4ai") { + Product = Sema4Mode() + } else { + Product = LegacyMode() + } NoTempManagement = set.Member(args, "--no-temp-management") NoPycManagement = set.Member(args, "--no-pyc-management") if set.Member(args, "--debug") { @@ -112,17 +116,6 @@ func RandomIdentifier() string { return randomIdentifier } -func RobocorpHome() string { - if len(ForcedRobocorpHome) > 0 { - return ExpandPath(ForcedRobocorpHome) - } - home := os.Getenv(ROBOCORP_HOME_VARIABLE) - if len(home) > 0 { - return ExpandPath(home) - } - return ExpandPath(defaultRobocorpLocation) -} - func DisableTempManagement() bool { return NoTempManagement || len(os.Getenv(RCC_NO_TEMP_MANAGEMENT)) > 0 } @@ -141,7 +134,7 @@ func RccRemoteAuthorization() (string, bool) { } func RobocorpLock() string { - return filepath.Join(RobocorpHome(), "robocorp.lck") + return filepath.Join(Product.Home(), "robocorp.lck") } func IsBundled() bool { @@ -177,7 +170,7 @@ func BinRcc() string { } func OldEventJournal() string { - return filepath.Join(RobocorpHome(), "event.log") + return filepath.Join(Product.Home(), "event.log") } func EventJournal() string { @@ -185,15 +178,15 @@ func EventJournal() string { } func JournalLocation() string { - return filepath.Join(RobocorpHome(), "journals") + return filepath.Join(Product.Home(), "journals") } func TemplateLocation() string { - return filepath.Join(RobocorpHome(), "templates") + return filepath.Join(Product.Home(), "templates") } func RobocorpTempRoot() string { - return filepath.Join(RobocorpHome(), "temp") + return filepath.Join(Product.Home(), "temp") } func RobocorpTempName() string { @@ -214,23 +207,19 @@ func RobocorpTemp() string { } func BinLocation() string { - return filepath.Join(RobocorpHome(), "bin") + return filepath.Join(Product.Home(), "bin") } func MicromambaLocation() string { - return filepath.Join(RobocorpHome(), "micromamba") + return filepath.Join(Product.Home(), "micromamba") } func SharedMarkerLocation() string { - return filepath.Join(HoloLocation(), "shared.yes") -} - -func HoloLocation() string { - return ExpandPath(defaultHoloLocation) + return filepath.Join(Product.HoloLocation(), "shared.yes") } func HoloInitLocation() string { - return filepath.Join(HoloLocation(), "lib", "catalog", "init") + return filepath.Join(Product.HoloLocation(), "lib", "catalog", "init") } func HoloInitUserFile() string { @@ -243,16 +232,16 @@ func HoloInitCommonFile() string { func HolotreeLocation() string { if SharedHolotree { - return HoloLocation() + return Product.HoloLocation() } - return filepath.Join(RobocorpHome(), "holotree") + return filepath.Join(Product.Home(), "holotree") } func HololibLocation() string { if SharedHolotree { - return filepath.Join(HoloLocation(), "lib") + return filepath.Join(Product.HoloLocation(), "lib") } - return filepath.Join(RobocorpHome(), "hololib") + return filepath.Join(Product.Home(), "hololib") } func HololibPids() string { @@ -285,9 +274,9 @@ func BadHololibSitePackagesLocation() string { func BadHololibScriptsLocation() string { if SharedHolotree { - return filepath.Join(HoloLocation(), "Scripts") + return filepath.Join(Product.HoloLocation(), "Scripts") } - return filepath.Join(RobocorpHome(), "Scripts") + return filepath.Join(Product.Home(), "Scripts") } func UsesHolotree() bool { @@ -295,23 +284,23 @@ func UsesHolotree() bool { } func UvCache() string { - return filepath.Join(RobocorpHome(), "uvcache") + return filepath.Join(Product.Home(), "uvcache") } func PipCache() string { - return filepath.Join(RobocorpHome(), "pipcache") + return filepath.Join(Product.Home(), "pipcache") } func WheelCache() string { - return filepath.Join(RobocorpHome(), "wheels") + return filepath.Join(Product.Home(), "wheels") } func RobotCache() string { - return filepath.Join(RobocorpHome(), "robots") + return filepath.Join(Product.Home(), "robots") } func MambaRootPrefix() string { - return RobocorpHome() + return Product.Home() } func MambaPackages() string { @@ -319,19 +308,19 @@ func MambaPackages() string { } func PipRcFile() string { - return ExpandPath(filepath.Join(RobocorpHome(), "piprc")) + return ExpandPath(filepath.Join(Product.Home(), "piprc")) } func MicroMambaRcFile() string { - return ExpandPath(filepath.Join(RobocorpHome(), "micromambarc")) + return ExpandPath(filepath.Join(Product.Home(), "micromambarc")) } func SettingsFile() string { - return ExpandPath(filepath.Join(RobocorpHome(), "settings.yaml")) + return ExpandPath(filepath.Join(Product.Home(), "settings.yaml")) } func CaBundleFile() string { - return ExpandPath(filepath.Join(RobocorpHome(), "ca-bundle.pem")) + return ExpandPath(filepath.Join(Product.Home(), "ca-bundle.pem")) } func DefineVerbosity(silent, debug, trace bool) { diff --git a/common/version.go b/common/version.go index 43034522..10d9b3e0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.29.1` + Version = `v18.0.0` ) diff --git a/conda/activate.go b/conda/activate.go index 6c0971cc..fa7249bf 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -55,7 +55,7 @@ func createScript(targetFolder string) (string, error) { } details := make(map[string]string) details["Rcc"] = common.BinRcc() - details["Robocorphome"] = common.RobocorpHome() + details["Robocorphome"] = common.Product.Home() details["MambaRootPrefix"] = common.MambaRootPrefix() details["Micromamba"] = BinMicromamba() details["Live"] = targetFolder diff --git a/conda/cleanup.go b/conda/cleanup.go index 791a0b63..d9ec22da 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -53,9 +53,9 @@ func bugsCleanup(dryrun bool) { } func alwaysCleanup(dryrun bool) { - base := filepath.Join(common.RobocorpHome(), "base") - live := filepath.Join(common.RobocorpHome(), "live") - miniconda3 := filepath.Join(common.RobocorpHome(), "miniconda3") + base := filepath.Join(common.Product.Home(), "base") + live := filepath.Join(common.Product.Home(), "live") + miniconda3 := filepath.Join(common.Product.Home(), "miniconda3") if dryrun { common.Log("Would be removing:") common.Log("- %v", base) @@ -112,7 +112,7 @@ func spotlessCleanup(dryrun, noCompress bool) (err error) { defer fail.Around(&err) fail.Fast(quickCleanup(dryrun)) - rcccache := filepath.Join(common.RobocorpHome(), "rcccache.yaml") + rcccache := filepath.Join(common.Product.Home(), "rcccache.yaml") if dryrun { common.Log("- %v", common.BinLocation()) common.Log("- %v", common.MicromambaLocation()) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 14d1f77d..6b724ab6 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -74,7 +74,7 @@ func IsWindows() bool { } func HasLongPathSupport() bool { - baseline := []string{common.RobocorpHome(), fmt.Sprintf("stump%x", os.Getpid())} + baseline := []string{common.Product.Home(), fmt.Sprintf("stump%x", os.Getpid())} stumpath := filepath.Join(baseline...) defer os.RemoveAll(stumpath) diff --git a/conda/robocorp.go b/conda/robocorp.go index ac069a49..9065dc14 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -196,7 +196,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", - "ROBOCORP_HOME="+common.RobocorpHome(), + fmt.Sprintf("%s=%s", common.Product.HomeVariable(), common.Product.Home()), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_HOLOTREE_SPACE_ROOT="+location, @@ -295,7 +295,7 @@ func HasMicroMamba() bool { } func LocalChannel() (string, bool) { - basefolder := filepath.Join(common.RobocorpHome(), "channel") + basefolder := filepath.Join(common.Product.Home(), "channel") fullpath := filepath.Join(basefolder, "channeldata.json") stats, err := os.Stat(fullpath) if err != nil { diff --git a/conda/validate.go b/conda/validate.go index a1689e6e..914a6329 100644 --- a/conda/validate.go +++ b/conda/validate.go @@ -1,6 +1,7 @@ package conda import ( + "fmt" "regexp" "github.com/robocorp/rcc/common" @@ -36,7 +37,7 @@ func ValidateLocations() bool { checked := map[string]string{ //"Environment variable 'TMP'": os.Getenv("TMP"), //"Environment variable 'TEMP'": os.Getenv("TEMP"), - "Path to 'ROBOCORP_HOME' directory": common.RobocorpHome(), + fmt.Sprintf("Path to '%s' directory", common.Product.HomeVariable()): common.Product.Home(), } // 7.1.2021 -- just warnings for now -- JMP:FIXME:JMP later validateLocations(checked) diff --git a/conda/workflows.go b/conda/workflows.go index 9f1dd10c..053fb352 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -120,7 +120,7 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) renameRemove(targetFolder) - location := filepath.Join(common.RobocorpHome(), "pkgs") + location := filepath.Join(common.Product.Home(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) common.Log("%sWARNING! To fix it, try to remove directory: %v%s", pretty.Red, location, pretty.Reset) return true diff --git a/docs/changelog.md b/docs/changelog.md index 74696670..69e4def6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v18.0.0 (date: 3.6.2024) WORK IN PROGRESS + +- MAJOR breaking change: rcc will now live in two product domains, + Robocorp and Sema4.ai +- feature: initial support for `--sema4ai` strategy selection +- robot tests to test Sema4.ai support + ## v17.29.1 (date: 29.5.2024) - bugfix: when taking locks, some of those need to be in shared directory, diff --git a/htfs/directory.go b/htfs/directory.go index 9e970d8a..89ed7ce9 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -33,7 +33,7 @@ func init() { killfile[".gitignore"] = true if !common.WarrantyVoided() { - pathlib.MakeSharedDir(common.HoloLocation()) + pathlib.MakeSharedDir(common.Product.HoloLocation()) pathlib.MakeSharedDir(common.HololibCatalogLocation()) pathlib.MakeSharedDir(common.HololibLibraryLocation()) pathlib.MakeSharedDir(common.HololibUsageLocation()) diff --git a/htfs/library.go b/htfs/library.go index f4265986..c8ab2b48 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -468,7 +468,7 @@ func New() (MutableLibrary, error) { if err != nil { return nil, err } - basedir := common.RobocorpHome() + basedir := common.Product.Home() identity := strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)) return &hololib{ identity: common.Sipit([]byte(identity)), diff --git a/htfs/virtual.go b/htfs/virtual.go index c9c9d077..64499cd1 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -22,7 +22,7 @@ type virtual struct { func Virtual() MutableLibrary { return &virtual{ - identity: common.Sipit([]byte(common.RobocorpHome())), + identity: common.Sipit([]byte(common.Product.Home())), } } diff --git a/operations/cache.go b/operations/cache.go index 89168ef7..63eddfd8 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -62,7 +62,7 @@ func cacheLocation() string { if len(reference) > 0 { return filepath.Join(filepath.Dir(reference), "rcccache.yaml") } else { - return filepath.Join(common.RobocorpHome(), "rcccache.yaml") + return filepath.Join(common.Product.Home(), "rcccache.yaml") } } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 64178675..e7fea394 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -67,7 +67,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["rcc.bin"] = common.BinRcc() result.Details["micromamba"] = conda.MicromambaVersion() result.Details["micromamba.bin"] = conda.BinMicromamba() - result.Details["ROBOCORP_HOME"] = common.RobocorpHome() + result.Details[common.Product.HomeVariable()] = common.Product.Home() result.Details["ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS"] = fmt.Sprintf("%v", common.OverrideSystemRequirements()) result.Details["RCC_VERBOSE_ENVIRONMENT_BUILDING"] = fmt.Sprintf("%v", common.VerboseEnvironmentBuilding()) result.Details["RCC_REMOTE_ORIGIN"] = fmt.Sprintf("%v", common.RccRemoteOrigin()) @@ -95,7 +95,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["config-ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) result.Details["config-ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) result.Details["config-legacy-renegotiation-allowed"] = fmt.Sprintf("%v", settings.Global.LegacyRenegotiation()) - result.Details["os-holo-location"] = common.HoloLocation() + result.Details["os-holo-location"] = common.Product.HoloLocation() result.Details["hololib-location"] = common.HololibLocation() result.Details["hololib-catalog-location"] = common.HololibCatalogLocation() result.Details["hololib-library-location"] = common.HololibLibraryLocation() @@ -128,13 +128,13 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { // checks if common.SharedHolotree { - result.Checks = append(result.Checks, verifySharedDirectory(common.HoloLocation())) + result.Checks = append(result.Checks, verifySharedDirectory(common.Product.HoloLocation())) result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLocation())) result.Checks = append(result.Checks, verifySharedDirectory(common.HololibCatalogLocation())) result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLibraryLocation())) } - result.Checks = append(result.Checks, robocorpHomeCheck()) - check := robocorpHomeMemberCheck() + result.Checks = append(result.Checks, productHomeCheck()) + check := productHomeMemberCheck() if check != nil { result.Checks = append(result.Checks, check) } @@ -372,7 +372,7 @@ func workdirCheck() *common.DiagnosticCheck { if err != nil { return nil } - inside, err := common.IsInsideRobocorpHome(workarea) + inside, err := common.IsInsideProductHome(workarea) if err != nil { return nil } @@ -381,14 +381,14 @@ func workdirCheck() *common.DiagnosticCheck { Type: "RPA", Category: common.CategoryPathCheck, Status: statusWarning, - Message: fmt.Sprintf("Working directory %q is inside ROBOCORP_HOME (%s).", workarea, common.RobocorpHome()), + Message: fmt.Sprintf("Working directory %q is inside %s (%s).", workarea, common.Product.HomeVariable(), common.Product.Home()), Link: supportGeneralUrl, } } return nil } -func robocorpHomeMemberCheck() *common.DiagnosticCheck { +func productHomeMemberCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") cache, err := SummonCache() if err != nil || len(cache.Users) < 2 { @@ -397,42 +397,42 @@ func robocorpHomeMemberCheck() *common.DiagnosticCheck { members := strings.Join(cache.Users, ", ") return &common.DiagnosticCheck{ Type: "RPA", - Category: common.CategoryRobocorpHomeMembers, + Category: common.CategoryProductHomeMembers, Status: statusWarning, - Message: fmt.Sprintf("More than one user is sharing ROBOCORP_HOME (%s). Those users are: %s.", common.RobocorpHome(), members), + Message: fmt.Sprintf("More than one user is sharing %s (%s). Those users are: %s.", common.Product.HomeVariable(), common.Product.Home(), members), Link: supportGeneralUrl, } } -func robocorpHomeCheck() *common.DiagnosticCheck { +func productHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") - if !conda.ValidLocation(common.RobocorpHome()) { + if !conda.ValidLocation(common.Product.Home()) { return &common.DiagnosticCheck{ Type: "RPA", - Category: common.CategoryRobocorpHome, + Category: common.CategoryProductHome, Status: statusFatal, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", common.RobocorpHome()), + Message: fmt.Sprintf("%s (%s) contains characters that makes RPA fail.", common.Product.HomeVariable(), common.Product.Home()), Link: supportGeneralUrl, } } userhome, err := os.UserHomeDir() if err == nil { - inside, err := common.IsInsideRobocorpHome(userhome) + inside, err := common.IsInsideProductHome(userhome) if err == nil && inside { return &common.DiagnosticCheck{ Type: "RPA", - Category: common.CategoryRobocorpHome, + Category: common.CategoryProductHome, Status: statusWarning, - Message: fmt.Sprintf("User home directory %q is inside ROBOCORP_HOME (%s).", userhome, common.RobocorpHome()), + Message: fmt.Sprintf("User home directory %q is inside %s (%s).", userhome, common.Product.HomeVariable(), common.Product.Home()), Link: supportGeneralUrl, } } } return &common.DiagnosticCheck{ Type: "RPA", - Category: common.CategoryRobocorpHome, + Category: common.CategoryProductHome, Status: statusOk, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", common.RobocorpHome()), + Message: fmt.Sprintf("%s (%s) is good enough.", common.Product.HomeVariable(), common.Product.Home()), Link: supportGeneralUrl, } } diff --git a/operations/running.go b/operations/running.go index 624f3012..02c227fd 100644 --- a/operations/running.go +++ b/operations/running.go @@ -180,10 +180,10 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. journal.ForRun(filepath.Join(config.ArtifactDirectory(), "journal.run")) cache, err := SummonCache() if err == nil && len(cache.Userset()) > 1 { - pretty.Note("There seems to be multiple users sharing ROBOCORP_HOME, which might cause problems.") + pretty.Note("There seems to be multiple users sharing %s, which might cause problems.", common.Product.HomeVariable()) pretty.Note("These are the users: %s.", cache.Userset()) - pretty.Highlight("To correct this problem, make sure that there is only one user per ROBOCORP_HOME.") - common.RunJournal("sharing", fmt.Sprintf("name=%s from=%s users=%s", theTask, packfile, cache.Userset()), "multiple users shareing ROBOCORP_HOME") + pretty.Highlight("To correct this problem, make sure that there is only one user per %s.", common.Product.HomeVariable()) + common.RunJournal("sharing", fmt.Sprintf("name=%s from=%s users=%s", theTask, packfile, cache.Userset()), fmt.Sprintf("multiple users shareing %s", common.Product.HomeVariable())) } common.RunJournal("start task", fmt.Sprintf("name=%s from=%s", theTask, packfile), "at task environment setup") diff --git a/robot/robot.go b/robot/robot.go index ca41a6fc..92b5550a 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -190,9 +190,9 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose := target.Diagnose("Robot") it.diagnoseTasks(diagnose) it.diagnoseVariousPaths(diagnose) - inside, err := common.IsInsideRobocorpHome(it.WorkingDirectory()) + inside, err := common.IsInsideProductHome(it.WorkingDirectory()) if err == nil && inside { - diagnose.Warning(0, "", "Robot working directory %q is inside ROBOCORP_HOME (%s)", it.WorkingDirectory(), common.RobocorpHome()) + diagnose.Warning(0, "", "Robot working directory %q is inside %s (%s)", it.WorkingDirectory(), common.Product.HomeVariable(), common.Product.Home()) } if it.Artifacts == "" { diagnose.Fail(0, "", "In robot.yaml, 'artifactsDir:' is required!") diff --git a/robot_tests/bare_action/package.yaml b/robot_tests/bare_action/package.yaml new file mode 100644 index 00000000..2f011f63 --- /dev/null +++ b/robot_tests/bare_action/package.yaml @@ -0,0 +1,14 @@ +name: RCC testing package +description: Just for testing rcc. +version: 0.0.1 + +documentation: https://github.com/robocorp/rcc/ + +dependencies: + conda-forge: + - python=3.10.12 + - uv=0.2.5 + pypi: + - robocorp=1.6.2 + - robocorp-actions=0.0.7 + diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 670a681d..fa26e992 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v17. + Must Have v18. Goal: There is debug message when bundled case. Step build/rcc version --controller citests --debug --bundled diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index 87a0d8b8..8c9c072a 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -2,7 +2,6 @@ Library OperatingSystem Library supporting.py Resource resources.robot -Default tags WIP *** Test cases *** diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 2f55530a..1d444d14 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -7,10 +7,19 @@ Library supporting.py Clean Local Remove Directory tmp/robocorp True +Prepare Sema4.ai Home + [Arguments] ${location} + Create Directory ${location} + Remove Environment Variable ROBOCORP_HOME + Set Environment Variable SEMA4AI_HOME ${location} + Copy File robot_tests/settings.yaml ${location}/settings.yaml + Fire And Forget build/rcc --sema4ai ht init --revoke --controller citests + Prepare Robocorp Home [Arguments] ${location} Create Directory ${location} - Set Environment Variable ROBOCORP_HOME ${location} + Remove Environment Variable SEMA4AI_HOME + Set Environment Variable ROBOCORP_HOME ${location} Copy File robot_tests/settings.yaml ${location}/settings.yaml Prepare Local diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot new file mode 100644 index 00000000..7b6aec13 --- /dev/null +++ b/robot_tests/sema4ai.robot @@ -0,0 +1,51 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot +Suite Setup Sema4.ai setup +Suite Teardown Sema4.ai teardown +Default tags WIP + +*** Keywords *** +Sema4.ai setup + Remove Directory tmp/sema4home True + Prepare Sema4.ai Home tmp/sema4home + +Sema4.ai teardown + Remove Directory tmp/sema4home True + Prepare Robocorp Home tmp/robocorp + +*** Test cases *** + +Goal: See rcc toplevel help for Sema4.ai + Step build/rcc --sema4ai --controller citests --help + Must Have SEMA4AI + Must Have completion + Wont Have ROBOCORP + Wont Have Robot + Wont Have robot + Wont Have bash + Wont Have fish + +Goal: See rcc commands for Sema4.ai + Step build/rcc --sema4ai --controller citests + Use STDERR + Must Have SEMA4AI + Wont Have ROBOCORP + Wont Have Robot + Wont Have robot + Wont Have completion + Wont Have bash + Wont Have fish + +Goal: Create package.yaml environment using uv + Step build/rcc --sema4ai ht vars -s sema4ai --controller citests robot_tests/bare_action/package.yaml + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have SEMA4AI_HOME= + Wont Have ROBOCORP_HOME= + Must Have _4e67cd8_81359368 + Use STDERR + Must Have Progress: 01/15 + Must Have Progress: 15/15 + Must Have Running uv install phase. diff --git a/settings/profile.go b/settings/profile.go index aa0c3fa4..c05a6278 100644 --- a/settings/profile.go +++ b/settings/profile.go @@ -47,7 +47,7 @@ func (it *Profile) LoadFrom(filename string) error { func (it *Profile) Import() (err error) { basename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(it.Name)) - filename := common.ExpandPath(filepath.Join(common.RobocorpHome(), basename)) + filename := common.ExpandPath(filepath.Join(common.Product.Home(), basename)) return it.SaveAs(filename) } From c88789c39db405dbffe29a42b520c442a6096f8b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 5 Jun 2024 13:25:17 +0300 Subject: [PATCH 503/516] Improvement: sema4ai product name (v18.0.1) - added company name as strategy name (dynamic name handling for user messages) - replaced static "Robocorp" references with strategy name - renamed some Robocorp functions to more generic Product functions --- cmd/assistant.go | 8 +++++--- cmd/cloud.go | 9 ++++++--- cmd/cloudNew.go | 4 ++-- cmd/community.go | 4 +++- cmd/configure.go | 5 ++++- cmd/configureprofile.go | 16 ++++++++-------- cmd/credentials.go | 9 +++++---- cmd/download.go | 6 ++++-- cmd/holotreeVariables.go | 2 +- cmd/issue.go | 6 ++++-- cmd/metric.go | 6 ++++-- cmd/pull.go | 4 ++-- cmd/push.go | 4 ++-- cmd/rcc/main.go | 4 ++-- cmd/robot.go | 6 ++++-- cmd/root.go | 8 ++++---- cmd/speed.go | 2 +- cmd/task.go | 6 ++++-- cmd/upload.go | 6 ++++-- cmd/userinfo.go | 5 +++-- common/strategies.go | 11 +++++++++++ common/variables.go | 12 ++++++------ common/version.go | 2 +- conda/cleanup.go | 10 +++++----- conda/condayaml.go | 2 +- conda/platform_darwin.go | 2 +- conda/platform_linux.go | 2 +- conda/platform_windows.go | 2 +- conda/robocorp.go | 6 +++--- conda/workflows.go | 2 +- docs/changelog.md | 6 ++++++ htfs/functions.go | 2 +- htfs/library.go | 2 +- operations/authorize.go | 8 ++++---- operations/authorize_test.go | 6 +++--- operations/diagnostics.go | 2 +- operations/issues.go | 4 ++-- remotree/listings.go | 2 +- robot_tests/sema4ai.robot | 2 ++ 39 files changed, 124 insertions(+), 81 deletions(-) diff --git a/cmd/assistant.go b/cmd/assistant.go index a1a10b4d..505efe77 100644 --- a/cmd/assistant.go +++ b/cmd/assistant.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -9,14 +11,14 @@ var assistantCmd = &cobra.Command{ Use: "assistant", Aliases: []string{"assist", "a"}, Short: "Group of commands related to `robot assistant`.", - Long: `This set of commands relate to Robocorp Robot Assistant related tasks. -They are either local, or in relation to Robocorp Control Room and tooling.`, + Long: fmt.Sprintf(`This set of commands relate to %s Robot Assistant related tasks. +They are either local, or in relation to %s Control Room and tooling.`, common.Product.Name(), common.Product.Name()), } func init() { if common.Product.IsLegacy() { rootCmd.AddCommand(assistantCmd) - assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", "Account used for Robocorp Control Room operations.") + assistantCmd.PersistentFlags().StringVarP(&accountName, "account", "", "", fmt.Sprintf("Account used for %s Control Room operations.", common.Product.Name())) } } diff --git a/cmd/cloud.go b/cmd/cloud.go index cd98cdcd..a7bd56a8 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -1,18 +1,21 @@ package cmd import ( + "fmt" + + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) var cloudCmd = &cobra.Command{ Use: "cloud", Aliases: []string{"robocorp", "c"}, - Short: "Group of commands related to `Robocorp Control Room`.", - Long: `This group of commands apply to communication with Robocorp Control Room.`, + Short: fmt.Sprintf("Group of commands related to `%s Control Room`.", common.Product.Name()), + Long: fmt.Sprintf(`This group of commands apply to communication with %s Control Room.`, common.Product.Name()), } func init() { rootCmd.AddCommand(cloudCmd) - cloudCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room operations.") + cloudCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", fmt.Sprintf("Account used for %s Control Room operations.", common.Product.Name())) } diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index 03ed1f77..a8dd8af2 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -15,8 +15,8 @@ import ( var newCloudCmd = &cobra.Command{ Use: "new", - Short: "Create a new robot into Robocorp Control Room.", - Long: "Create a new robot into Robocorp Control Room.", + Short: fmt.Sprintf("Create a new robot into %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Create a new robot into %s Control Room.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("New robot creation lasted").Report() diff --git a/cmd/community.go b/cmd/community.go index 402d21e2..46f636f4 100644 --- a/cmd/community.go +++ b/cmd/community.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -8,7 +10,7 @@ import ( var communityCmd = &cobra.Command{ Use: "community", Aliases: []string{"co"}, - Short: "Group of commands related to `Robocorp Community`.", + Short: fmt.Sprintf("Group of commands related to `%s Community`.", common.Product.Name()), Long: `This group of commands apply to community provided robots and services.`, } diff --git a/cmd/configure.go b/cmd/configure.go index fe0515db..470713ec 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,6 +1,9 @@ package cmd import ( + "fmt" + + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -14,5 +17,5 @@ var configureCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configureCmd) - configureCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", "Account used for Robocorp Control Room task.") + configureCmd.PersistentFlags().StringVarP(&accountName, "account", "a", "", fmt.Sprintf("Account used for %s Control Room task.", common.Product.Name())) } diff --git a/cmd/configureprofile.go b/cmd/configureprofile.go index fc2bd8e2..db2d717a 100644 --- a/cmd/configureprofile.go +++ b/cmd/configureprofile.go @@ -82,8 +82,8 @@ func cleanupProfile() { var configureSwitchCmd = &cobra.Command{ Use: "switch", - Short: "Switch active configuration profile for Robocorp tooling.", - Long: "Switch active configuration profile for Robocorp tooling.", + Short: fmt.Sprintf("Switch active configuration profile for %s tooling.", common.Product.Name()), + Long: fmt.Sprintf("Switch active configuration profile for %s tooling.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Configuration switch lasted").Report() @@ -108,8 +108,8 @@ var configureSwitchCmd = &cobra.Command{ var configureRemoveCmd = &cobra.Command{ Use: "remove", - Short: "Remove named a configuration profile for Robocorp tooling.", - Long: "Remove named a configuration profile for Robocorp tooling.", + Short: fmt.Sprintf("Remove named a configuration profile for %s tooling.", common.Product.Name()), + Long: fmt.Sprintf("Remove named a configuration profile for %s tooling.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Configuration remove lasted").Report() @@ -129,8 +129,8 @@ var configureRemoveCmd = &cobra.Command{ var configureExportCmd = &cobra.Command{ Use: "export", - Short: "Export a configuration profile for Robocorp tooling.", - Long: "Export a configuration profile for Robocorp tooling.", + Short: fmt.Sprintf("Export a configuration profile for %s tooling.", common.Product.Name()), + Long: fmt.Sprintf("Export a configuration profile for %s tooling.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Configuration export lasted").Report() @@ -144,8 +144,8 @@ var configureExportCmd = &cobra.Command{ var configureImportCmd = &cobra.Command{ Use: "import", - Short: "Import a configuration profile for Robocorp tooling.", - Long: "Import a configuration profile for Robocorp tooling.", + Short: fmt.Sprintf("Import a configuration profile for %s tooling.", common.Product.Name()), + Long: fmt.Sprintf("Import a configuration profile for %s tooling.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Configuration import lasted").Report() diff --git a/cmd/credentials.go b/cmd/credentials.go index 6b5eb151..78ac5853 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "strings" "time" @@ -19,8 +20,8 @@ var ( var credentialsCmd = &cobra.Command{ Use: "credentials [credentials]", - Short: "Manage Robocorp Control Room API credentials.", - Long: "Manage Robocorp Control Room API credentials for later use.", + Short: fmt.Sprintf("Manage %s Control Room API credentials.", common.Product.Name()), + Long: fmt.Sprintf("Manage %s Control Room API credentials for later use.", common.Product.Name()), Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { @@ -55,7 +56,7 @@ var credentialsCmd = &cobra.Command{ } parts := strings.Split(credentials, ":") if len(parts) != 2 { - pretty.Exit(1, "Error: No valid credentials detected. Copy them from Robocorp Control Room.") + pretty.Exit(1, "Error: No valid credentials detected. Copy them from %s Control Room.", common.Product.Name()) } common.Log("Adding credentials: %v", parts) operations.UpdateCredentials(account, https, parts[0], parts[1]) @@ -85,5 +86,5 @@ func init() { credentialsCmd.Flags().BoolVarP(&defaultFlag, "default", "d", false, "Set this as the default account.") credentialsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") credentialsCmd.Flags().BoolVarP(&verifiedFlag, "verified", "v", false, "Updates the verified timestamp, if the credentials are still active.") - credentialsCmd.Flags().StringVarP(&endpointUrl, "endpoint", "e", "", "Robocorp Control Room endpoint used with the given account (or default).") + credentialsCmd.Flags().StringVarP(&endpointUrl, "endpoint", "e", "", fmt.Sprintf("%s Control Room endpoint used with the given account (or default).", common.Product.Name())) } diff --git a/cmd/download.go b/cmd/download.go index b7b50f64..2395545c 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" @@ -11,8 +13,8 @@ import ( var downloadCmd = &cobra.Command{ Use: "download", - Short: "Fetch an existing robot from Robocorp Control Room.", - Long: "Fetch an existing robot from Robocorp Control Room.", + Short: fmt.Sprintf("Fetch an existing robot from %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Fetch an existing robot from %s Control Room.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Download lasted").Report() diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 22b18595..fe75f4b1 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -75,7 +75,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) - condafile := filepath.Join(common.RobocorpTemp(), common.BlueprintHash(holotreeBlueprint)) + condafile := filepath.Join(common.ProductTemp(), common.BlueprintHash(holotreeBlueprint)) err = pathlib.WriteFile(condafile, holotreeBlueprint, 0o644) pretty.Guard(err == nil, 6, "%s", err) diff --git a/cmd/issue.go b/cmd/issue.go index 19e9e0b4..47f4d273 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" @@ -16,8 +18,8 @@ var ( var issueCmd = &cobra.Command{ Use: "issue", - Short: "Send an issue to Robocorp Control Room via rcc.", - Long: "Send an issue to Robocorp Control Room via rcc.", + Short: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), + Long: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Feedback issue lasted").Report() diff --git a/cmd/metric.go b/cmd/metric.go index 43477b4f..7366028c 100644 --- a/cmd/metric.go +++ b/cmd/metric.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" @@ -17,8 +19,8 @@ var ( var metricCmd = &cobra.Command{ Use: "metric", - Short: "Send some metric to Robocorp Control Room.", - Long: "Send some metric to Robocorp Control Room.", + Short: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Feedback metric lasted").Report() diff --git a/cmd/pull.go b/cmd/pull.go index 6abeb5fe..f69538b4 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -16,8 +16,8 @@ import ( var pullCmd = &cobra.Command{ Use: "pull", - Short: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", - Long: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", + Short: fmt.Sprintf("Pull a robot from %s Control Room and unwrap it into local directory.", common.Product.Name()), + Long: fmt.Sprintf("Pull a robot from %s Control Room and unwrap it into local directory.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Pull lasted").Report() diff --git a/cmd/push.go b/cmd/push.go index 7606ebcf..6421098a 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -16,8 +16,8 @@ import ( var pushCmd = &cobra.Command{ Use: "push", - Short: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", - Long: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", + Short: fmt.Sprintf("Wrap the local directory and push it into %s Control Room as a specific robot.", common.Product.Name()), + Long: fmt.Sprintf("Wrap the local directory and push it into %s Control Room as a specific robot.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Push lasted").Report() diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index db61331a..d977fcf9 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -97,7 +97,7 @@ func startTempRecycling() { return } defer common.Timeline("temp recycling done") - pattern := filepath.Join(common.RobocorpTempRoot(), "*", "recycle.now") + pattern := filepath.Join(common.ProductTempRoot(), "*", "recycle.now") found, err := filepath.Glob(pattern) if err != nil { common.Debug("Recycling failed, reason: %v", err) @@ -121,7 +121,7 @@ func markTempForRecycling() { if markedAlready { return } - target := common.RobocorpTempName() + target := common.ProductTempName() if pathlib.Exists(target) { filename := filepath.Join(target, "recycle.now") pathlib.WriteFile(filename, []byte("True"), 0o644) diff --git a/cmd/robot.go b/cmd/robot.go index cdd7b487..db7befc5 100644 --- a/cmd/robot.go +++ b/cmd/robot.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -9,8 +11,8 @@ var robotCmd = &cobra.Command{ Use: "robot", Aliases: []string{"r"}, Short: "Group of commands related to `robot`.", - Long: `This set of commands relate to Robocorp Control Room related tasks. They are -executed either locally, or in connection to Robocorp Control Room and tooling.`, + Long: fmt.Sprintf(`This set of commands relate to %s Control Room related tasks. They are +executed either locally, or in connection to %s Control Room and tooling.`, common.Product.Name(), common.Product.Name()), } func init() { diff --git a/cmd/root.go b/cmd/root.go index 524b7626..1cd9a50c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,10 +66,10 @@ func commandTree(level int, prefix string, parent *cobra.Command) { var rootCmd = &cobra.Command{ Use: "rcc", - Short: "rcc is environment manager for Robocorp Automation Stack", - Long: `rcc provides support for creating and managing tasks, -communicating with Robocorp Control Room, and managing virtual environments where -tasks can be developed, debugged, and run.`, + Short: fmt.Sprintf("rcc is environment manager for %s Automation Stack", common.Product.Name()), + Long: fmt.Sprintf(`rcc provides support for creating and managing tasks, +communicating with %s Control Room, and managing virtual environments where +tasks can be developed, debugged, and run.`, common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if versionFlag { common.Stdout("%s\n", common.Version) diff --git a/cmd/speed.go b/cmd/speed.go index f1b426ff..3dd5901a 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -69,7 +69,7 @@ var speedtestCmd = &cobra.Command{ common.DefineVerbosity(true, false, false) } go workingWorm(signal, timing, debug) - folder := common.RobocorpTemp() + folder := common.ProductTemp() pretty.DebugNote("Speed test will force temporary %s to be %q while testing.", common.Product.HomeVariable(), folder) err := os.RemoveAll(folder) pretty.Guard(err == nil, 4, "Error: %v", err) diff --git a/cmd/task.go b/cmd/task.go index 49a45469..bfc102cc 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -9,8 +11,8 @@ var taskCmd = &cobra.Command{ Use: "task", Aliases: []string{"t"}, Short: "Group of commands related to `task`.", - Long: `This set of commands relate to Robocorp Control Room related tasks. They are -executed either locally, or in connection to Robocorp Control Room and tooling.`, + Long: fmt.Sprintf(`This set of commands relate to %s Control Room related tasks. They are +executed either locally, or in connection to %s Control Room and tooling.`, common.Product.Name(), common.Product.Name()), } func init() { diff --git a/cmd/upload.go b/cmd/upload.go index 5eaa2701..be964269 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" @@ -11,8 +13,8 @@ import ( var uploadCmd = &cobra.Command{ Use: "upload", - Short: "Push an existing robot to Robocorp Control Room.", - Long: "Push an existing robot to Robocorp Control Room.", + Short: fmt.Sprintf("Push an existing robot to %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Push an existing robot to %s Control Room.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Upload lasted").Report() diff --git a/cmd/userinfo.go b/cmd/userinfo.go index 286e5ac6..5dea96bb 100644 --- a/cmd/userinfo.go +++ b/cmd/userinfo.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "fmt" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -14,8 +15,8 @@ import ( var userinfoCmd = &cobra.Command{ Use: "userinfo", Aliases: []string{"user"}, - Short: "Query user information from Robocorp Control Room.", - Long: "Query user information from Robocorp Control Room.", + Short: fmt.Sprintf("Query user information from %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Query user information from %s Control Room.", common.Product.Name()), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { defer common.Stopwatch("Userinfo query lasted").Report() diff --git a/common/strategies.go b/common/strategies.go index 02b283d8..15b5e6a7 100644 --- a/common/strategies.go +++ b/common/strategies.go @@ -4,11 +4,14 @@ import "os" const ( ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + ROBOCORP_NAME = `Robocorp` SEMA4AI_HOME_VARIABLE = `SEMA4AI_HOME` + SEMA4AI_NAME = `Sema4.ai` ) type ( ProductStrategy interface { + Name() string IsLegacy() bool ForceHome(string) HomeVariable() string @@ -33,6 +36,10 @@ func Sema4Mode() ProductStrategy { return &sema4Strategy{} } +func (it *legacyStrategy) Name() string { + return ROBOCORP_NAME +} + func (it *legacyStrategy) IsLegacy() bool { return true } @@ -60,6 +67,10 @@ func (it *legacyStrategy) HoloLocation() string { return ExpandPath(defaultHoloLocation) } +func (it *sema4Strategy) Name() string { + return SEMA4AI_NAME +} + func (it *sema4Strategy) IsLegacy() bool { return false } diff --git a/common/variables.go b/common/variables.go index 0a19dc23..8a2d91b1 100644 --- a/common/variables.go +++ b/common/variables.go @@ -133,7 +133,7 @@ func RccRemoteAuthorization() (string, bool) { return result, len(result) > 0 } -func RobocorpLock() string { +func ProductLock() string { return filepath.Join(Product.Home(), "robocorp.lck") } @@ -185,16 +185,16 @@ func TemplateLocation() string { return filepath.Join(Product.Home(), "templates") } -func RobocorpTempRoot() string { +func ProductTempRoot() string { return filepath.Join(Product.Home(), "temp") } -func RobocorpTempName() string { - return filepath.Join(RobocorpTempRoot(), RandomIdentifier()) +func ProductTempName() string { + return filepath.Join(ProductTempRoot(), RandomIdentifier()) } -func RobocorpTemp() string { - tempLocation := RobocorpTempName() +func ProductTemp() string { + tempLocation := ProductTempName() fullpath, err := filepath.Abs(tempLocation) if err != nil { fullpath = tempLocation diff --git a/common/version.go b/common/version.go index 10d9b3e0..c9a88e85 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.0` + Version = `v18.0.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index d9ec22da..ce030ff3 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -88,14 +88,14 @@ func quickCleanup(dryrun bool) error { downloadCleanup(dryrun) if dryrun { common.Log("- %v", common.HolotreeLocation()) - common.Log("- %v", common.RobocorpTempRoot()) + common.Log("- %v", common.ProductTempRoot()) return nil } err := safeRemove("cache", common.HolotreeLocation()) if err != nil { return err } - return safeRemove("temp", common.RobocorpTempRoot()) + return safeRemove("temp", common.ProductTempRoot()) } func cleanupAllCaches(dryrun bool) error { @@ -139,7 +139,7 @@ func spotlessCleanup(dryrun, noCompress bool) (err error) { } func cleanupTemp(deadline time.Time, dryrun bool) error { - basedir := common.RobocorpTempRoot() + basedir := common.ProductTempRoot() handle, err := os.Open(basedir) if err != nil { return err @@ -178,7 +178,7 @@ func BugsCleanup() { func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress, caches bool) (err error) { defer fail.Around(&err) - lockfile := common.RobocorpLock() + lockfile := common.ProductLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment cleanup [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000, false) completed() @@ -223,7 +223,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads, noCompress } func RemoveCurrentTemp() { - target := common.RobocorpTempName() + target := common.ProductTempName() common.Debug("removing current temp %v", target) common.Timeline("removing current temp: %v", target) err := safeRemove("temp", target) diff --git a/conda/condayaml.go b/conda/condayaml.go index e3c0a346..e4af3154 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -645,7 +645,7 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b diagnose.Ok(0, "Pip dependencies in conda.yaml are ok.") } if floating { - diagnose.Warning(0, "", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") + diagnose.Warning(0, "", "Floating dependencies in %s Cloud containers will be slow, because floating environments cannot be cached.", common.Product.Name()) } } diff --git a/conda/platform_darwin.go b/conda/platform_darwin.go index 0b5d4f96..661eac76 100644 --- a/conda/platform_darwin.go +++ b/conda/platform_darwin.go @@ -33,7 +33,7 @@ func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) if !common.DisableTempManagement() { - tempFolder := common.RobocorpTemp() + tempFolder := common.ProductTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) } diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 00e1035d..0b6b7599 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -37,7 +37,7 @@ func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) if !common.DisableTempManagement() { - tempFolder := common.RobocorpTemp() + tempFolder := common.ProductTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) } diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 6b724ab6..b01516b4 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -42,7 +42,7 @@ func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) if !common.DisableTempManagement() { - tempFolder := common.RobocorpTemp() + tempFolder := common.ProductTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) } diff --git a/conda/robocorp.go b/conda/robocorp.go index 9065dc14..6d374eff 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -174,15 +174,15 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if !common.DisablePycManagement() { environment = append(environment, "PYTHONDONTWRITEBYTECODE=x", - "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), + "PYTHONPYCACHEPREFIX="+common.ProductTemp(), ) } else { common.Timeline(".pyc file management was disabled.") } if !common.DisableTempManagement() { environment = append(environment, - "TEMP="+common.RobocorpTemp(), - "TMP="+common.RobocorpTemp(), + "TEMP="+common.ProductTemp(), + "TMP="+common.ProductTemp(), ) } else { common.Timeline("temp directory management was disabled.") diff --git a/conda/workflows.go b/conda/workflows.go index 053fb352..44fd5b27 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -499,7 +499,7 @@ func temporaryConfig(condaYaml, requirementsText, filename string) (string, stri func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configuration string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) - lockfile := common.RobocorpLock() + lockfile := common.ProductLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000, false) completed() diff --git a/docs/changelog.md b/docs/changelog.md index 69e4def6..15ed0639 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.0.1 (date: 5.6.2024) WORK IN PROGRESS + +- added company name as strategy name (dynamic name handling for user messages) +- replaced static "Robocorp" references with strategy name +- renamed some Robocorp functions to more generic Product functions + ## v18.0.0 (date: 3.6.2024) WORK IN PROGRESS - MAJOR breaking change: rcc will now live in two product domains, diff --git a/htfs/functions.go b/htfs/functions.go index 6110f5a4..3458fb57 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -545,7 +545,7 @@ func LoadCatalogs() ([]string, Roots) { func CatalogLoader(catalog string, at int, roots Roots) anywork.Work { return func() { - tempdir := filepath.Join(common.RobocorpTemp(), "shadow") + tempdir := filepath.Join(common.ProductTemp(), "shadow") shadow, err := NewRoot(tempdir) if err != nil { panic(fmt.Sprintf("Temp dir %q, reason: %v", tempdir, err)) diff --git a/htfs/library.go b/htfs/library.go index c8ab2b48..9829e446 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -309,7 +309,7 @@ func (it *hololib) queryBlueprint(key string) bool { if !pathlib.IsFile(catalog) { return false } - tempdir := filepath.Join(common.RobocorpTemp(), key) + tempdir := filepath.Join(common.ProductTemp(), key) shadow, err := NewRoot(tempdir) if err != nil { return false diff --git a/operations/authorize.go b/operations/authorize.go index a6887c9f..0bd362f6 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -140,7 +140,7 @@ func WorkspaceToken(token string) string { return fmt.Sprintf("RC_WST %s", token) } -func RobocorpCloudHmac(identifier, token string) string { +func ProductCloudHmac(identifier, token string) string { return fmt.Sprintf("robocloud-hmac %s %s", identifier, token) } @@ -196,7 +196,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims, per signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson - request.Headers[authorization] = RobocorpCloudHmac(account.Identifier, signed) + request.Headers[authorization] = ProductCloudHmac(account.Identifier, signed) request.Headers[nonceHeader] = nonce request.Headers[contentLength] = fmt.Sprintf("%d", size) request.Body = strings.NewReader(body) @@ -229,7 +229,7 @@ func DeleteAccount(client cloud.Client, account *account) error { signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson - request.Headers[authorization] = RobocorpCloudHmac(account.Identifier, signed) + request.Headers[authorization] = ProductCloudHmac(account.Identifier, signed) request.Headers[nonceHeader] = nonce response := client.Delete(request) if response.Status < 200 || 299 < response.Status { @@ -246,7 +246,7 @@ func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson - request.Headers[authorization] = RobocorpCloudHmac(account.Identifier, signed) + request.Headers[authorization] = ProductCloudHmac(account.Identifier, signed) request.Headers[nonceHeader] = nonce response := client.Get(request) if response.Status != 200 { diff --git a/operations/authorize_test.go b/operations/authorize_test.go index 243bfd21..e2bd3662 100644 --- a/operations/authorize_test.go +++ b/operations/authorize_test.go @@ -58,11 +58,11 @@ func TestCanCreateBearerToken(t *testing.T) { must_be.Equal(operations.BearerToken("barbie"), "Bearer barbie") } -func TestCanCreateRobocorpCloudHmac(t *testing.T) { +func TestCanCreateProductCloudHmac(t *testing.T) { must_be, _ := hamlet.Specifications(t) - must_be.Equal(operations.RobocorpCloudHmac("11", "token"), "robocloud-hmac 11 token") - must_be.Equal(operations.RobocorpCloudHmac("1234", "abcd"), "robocloud-hmac 1234 abcd") + must_be.Equal(operations.ProductCloudHmac("11", "token"), "robocloud-hmac 11 token") + must_be.Equal(operations.ProductCloudHmac("1234", "abcd"), "robocloud-hmac 1234 abcd") } func TestCanCreateNewClaims(t *testing.T) { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index e7fea394..92a459b3 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -202,7 +202,7 @@ func lockfiles() map[string]string { result["lock-config"] = xviper.Lockfile() result["lock-cache"] = cacheLockFile() result["lock-holotree"] = common.HolotreeLock() - result["lock-robocorp"] = common.RobocorpLock() + result["lock-robocorp"] = common.ProductLock() result["lock-userlock"] = htfs.UserHolotreeLockfile() return result } diff --git a/operations/issues.go b/operations/issues.go index 8bec8901..0c512d62 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -31,7 +31,7 @@ func loadToken(reportFile string) (Token, error) { } func createIssueZip(attachmentsFiles []string) (string, error) { - zipfile := filepath.Join(common.RobocorpTemp(), "attachments.zip") + zipfile := filepath.Join(common.ProductTemp(), "attachments.zip") zipper, err := newZipper(zipfile) if err != nil { return "", err @@ -56,7 +56,7 @@ func createIssueZip(attachmentsFiles []string) (string, error) { } func createDiagnosticsReport(robotfile string) (string, *common.DiagnosticStatus, error) { - file := filepath.Join(common.RobocorpTemp(), "diagnostics.txt") + file := filepath.Join(common.ProductTemp(), "diagnostics.txt") diagnostics, err := ProduceDiagnostics(file, robotfile, false, false, false) if err != nil { return "", nil, err diff --git a/remotree/listings.go b/remotree/listings.go index b44e8f01..018e1203 100644 --- a/remotree/listings.go +++ b/remotree/listings.go @@ -54,7 +54,7 @@ func makeQueryHandler(queries Partqueries, triggers chan string) http.HandlerFun func loadSingleCatalog(catalog string) (root *htfs.Root, err error) { defer fail.Around(&err) - tempdir := filepath.Join(common.RobocorpTemp(), "rccremote") + tempdir := filepath.Join(common.ProductTemp(), "rccremote") shadow, err := htfs.NewRoot(tempdir) fail.On(err != nil, "Could not create root, reason: %v", err) filename := filepath.Join(common.HololibCatalogLocation(), catalog) diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot index 7b6aec13..a5688d90 100644 --- a/robot_tests/sema4ai.robot +++ b/robot_tests/sema4ai.robot @@ -22,6 +22,7 @@ Goal: See rcc toplevel help for Sema4.ai Must Have SEMA4AI Must Have completion Wont Have ROBOCORP + Wont Have Robocorp Wont Have Robot Wont Have robot Wont Have bash @@ -32,6 +33,7 @@ Goal: See rcc commands for Sema4.ai Use STDERR Must Have SEMA4AI Wont Have ROBOCORP + Wont Have Robocorp Wont Have Robot Wont Have robot Wont Have completion From a94a645ec40bed1be28c7c593e2b5e5b056552ac Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 7 Jun 2024 09:52:56 +0300 Subject: [PATCH 504/516] Improvement: settings.yaml in product strategy (v18.0.2) - default `settings.yaml` is now behind product strategy (each product can have their own default settings) --- .../{settings.yaml => robocorp_settings.yaml} | 2 +- assets/sema4ai_settings.yaml | 40 +++++++++++++++++++ blobs/asset_test.go | 5 ++- common/strategies.go | 9 +++++ common/version.go | 2 +- docs/changelog.md | 5 +++ robot_tests/sema4ai.robot | 7 ++++ settings/settings.go | 2 +- 8 files changed, 68 insertions(+), 4 deletions(-) rename assets/{settings.yaml => robocorp_settings.yaml} (94%) create mode 100644 assets/sema4ai_settings.yaml diff --git a/assets/settings.yaml b/assets/robocorp_settings.yaml similarity index 94% rename from assets/settings.yaml rename to assets/robocorp_settings.yaml index e006aac7..44568d27 100644 --- a/assets/settings.yaml +++ b/assets/robocorp_settings.yaml @@ -41,6 +41,6 @@ branding: meta: name: default - description: default settings.yaml internal to rcc + description: Robocorp.com default settings.yaml internal to rcc source: builtin version: 2023.09 diff --git a/assets/sema4ai_settings.yaml b/assets/sema4ai_settings.yaml new file mode 100644 index 00000000..f3294a04 --- /dev/null +++ b/assets/sema4ai_settings.yaml @@ -0,0 +1,40 @@ +endpoints: + cloud-api: https://api.eu1.robocorp.com/ + cloud-linking: https://cloud.robocorp.com/link/ + cloud-ui: https://cloud.robocorp.com/ + pypi: # https://pypi.org/simple/ + pypi-trusted: # https://pypi.org/ + conda: # https://conda.anaconda.org/ + downloads: https://downloads.robocorp.com/ + docs: https://robocorp.com/docs/ + telemetry: https://telemetry.robocorp.com/ + issues: https://telemetry.robocorp.com/ + +diagnostics-hosts: + - files.pythonhosted.org + - github.com + - conda.anaconda.org + - pypi.org + +autoupdates: + setup-utility: https://downloads.robocorp.com/setup-utility/releases/ + templates: https://downloads.robocorp.com/templates/templates.yaml + +certificates: + verify-ssl: true + ssl-no-revoke: false + legacy-renegotiation-allowed: false + +options: + no-build: false + +network: + no-proxy: # no no proxy by default + https-proxy: # no proxy by default + http-proxy: # no proxy by default + +meta: + name: default + description: Sema4.ai default settings.yaml internal to rcc + source: builtin + version: 2024.06 diff --git a/blobs/asset_test.go b/blobs/asset_test.go index a09bfb27..2e42bf08 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -27,12 +27,15 @@ func TestCanOtherAssets(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) must_be.Panic(func() { blobs.MustAsset("assets/missing.yaml") }) + must_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) + + wont_be.Panic(func() { blobs.MustAsset("assets/robocorp_settings.yaml") }) + wont_be.Panic(func() { blobs.MustAsset("assets/sema4ai_settings.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/micromamba_version.txt") }) wont_be.Panic(func() { blobs.MustAsset("assets/externally_managed.txt") }) wont_be.Panic(func() { blobs.MustAsset("assets/templates.yaml") }) - wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/speedtest.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/man/LICENSE.txt") }) diff --git a/common/strategies.go b/common/strategies.go index 15b5e6a7..c97bcad3 100644 --- a/common/strategies.go +++ b/common/strategies.go @@ -17,6 +17,7 @@ type ( HomeVariable() string Home() string HoloLocation() string + DefaultSettingsYamlFile() string } legacyStrategy struct { @@ -67,6 +68,10 @@ func (it *legacyStrategy) HoloLocation() string { return ExpandPath(defaultHoloLocation) } +func (it *legacyStrategy) DefaultSettingsYamlFile() string { + return "assets/robocorp_settings.yaml" +} + func (it *sema4Strategy) Name() string { return SEMA4AI_NAME } @@ -97,3 +102,7 @@ func (it *sema4Strategy) Home() string { func (it *sema4Strategy) HoloLocation() string { return ExpandPath(defaultSema4HoloLocation) } + +func (it *sema4Strategy) DefaultSettingsYamlFile() string { + return "assets/sema4ai_settings.yaml" +} diff --git a/common/version.go b/common/version.go index c9a88e85..1de02217 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.1` + Version = `v18.0.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 15ed0639..73900472 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v18.0.2 (date: 7.6.2024) WORK IN PROGRESS + +- default `settings.yaml` is now behind product strategy (each product can + have their own default settings) + ## v18.0.1 (date: 5.6.2024) WORK IN PROGRESS - added company name as strategy name (dynamic name handling for user messages) diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot index a5688d90..2b820d07 100644 --- a/robot_tests/sema4ai.robot +++ b/robot_tests/sema4ai.robot @@ -40,6 +40,13 @@ Goal: See rcc commands for Sema4.ai Wont Have bash Wont Have fish +Goal: Default settings.yaml for Sema4.ai + Step build/rcc --sema4ai configuration settings --controller citests + Must Have Sema4.ai default settings.yaml + Wont Have assistant + Wont Have branding + Wont Have logo + Goal: Create package.yaml environment using uv Step build/rcc --sema4ai ht vars -s sema4ai --controller citests robot_tests/bare_action/package.yaml Must Have RCC_ENVIRONMENT_HASH= diff --git a/settings/settings.go b/settings/settings.go index 83a2e794..26a3ddf5 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -39,7 +39,7 @@ func HasCustomSettings() bool { } func DefaultSettings() ([]byte, error) { - return blobs.Asset("assets/settings.yaml") + return blobs.Asset(common.Product.DefaultSettingsYamlFile()) } func DefaultSettingsLayer() *Settings { From 33dfd9e62acb8d6b94b4a5a5cc5bc454b96b8a3e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 7 Jun 2024 11:46:40 +0300 Subject: [PATCH 505/516] Bugfix: Window icacls hardocoded path (v18.0.3) - Windows bugfix: icacls now applied on shared holotree location from product strategy (was hardcoded before) --- cmd/command_windows.go | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/command_windows.go b/cmd/command_windows.go index 6cce83f4..93090b9a 100644 --- a/cmd/command_windows.go +++ b/cmd/command_windows.go @@ -18,7 +18,7 @@ func osSpecificHolotreeSharing(enable bool) { parent := filepath.Dir(common.Product.HoloLocation()) _, err := pathlib.ForceSharedDir(parent) pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) - task := shell.New(nil, ".", "icacls", "C:/ProgramData/robocorp", "/grant", "*S-1-5-32-545:(OI)(CI)M", "/T", "/Q") + task := shell.New(nil, ".", "icacls", parent, "/grant", "*S-1-5-32-545:(OI)(CI)M", "/T", "/Q") _, err = task.Execute(false) pretty.Guard(err == nil, 2, "Could not set 'icacls' settings, reason: %v", err) err = os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) diff --git a/common/version.go b/common/version.go index 1de02217..eda978d6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.2` + Version = `v18.0.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 73900472..6168a6ae 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v18.0.3 (date: 7.6.2024) WORK IN PROGRESS + +- Windows bugfix: icacls now applied on shared holotree location from product + strategy (was hardcoded before) + ## v18.0.2 (date: 7.6.2024) WORK IN PROGRESS - default `settings.yaml` is now behind product strategy (each product can From 6e5ee3940d3b49f7fb31ed9d9cdc3bba8623a1c0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 12 Jun 2024 14:47:24 +0300 Subject: [PATCH 506/516] Improvement: --robocorp flag (v18.0.4) - Additional `--robocorp` product flag added. To match `--sema4ai` flag. - Now using `%ProgramData%` instead of hard coded `c:\ProgramData\` in code. - Update on default `settings.yaml` for Sema4.ai products. --- assets/sema4ai_settings.yaml | 7 ------- cmd/root.go | 19 ++++++++++--------- common/platform_windows.go | 4 ++-- common/variables.go | 12 ++++++++++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ robot_tests/sema4ai.robot | 4 +++- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/assets/sema4ai_settings.yaml b/assets/sema4ai_settings.yaml index f3294a04..5c8a2ebf 100644 --- a/assets/sema4ai_settings.yaml +++ b/assets/sema4ai_settings.yaml @@ -2,9 +2,6 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://cloud.robocorp.com/link/ cloud-ui: https://cloud.robocorp.com/ - pypi: # https://pypi.org/simple/ - pypi-trusted: # https://pypi.org/ - conda: # https://conda.anaconda.org/ downloads: https://downloads.robocorp.com/ docs: https://robocorp.com/docs/ telemetry: https://telemetry.robocorp.com/ @@ -16,10 +13,6 @@ diagnostics-hosts: - conda.anaconda.org - pypi.org -autoupdates: - setup-utility: https://downloads.robocorp.com/setup-utility/releases/ - templates: https://downloads.robocorp.com/templates/templates.yaml - certificates: verify-ssl: true ssl-no-revoke: false diff --git a/cmd/root.go b/cmd/root.go index 1cd9a50c..02895555 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,14 +19,14 @@ import ( ) var ( - anythingIgnore string - profilefile string - profiling *os.File - versionFlag bool - silentFlag bool - debugFlag bool - traceFlag bool - sema4FakeFlag bool + anythingIgnore string + profilefile string + profiling *os.File + versionFlag bool + silentFlag bool + debugFlag bool + traceFlag bool + productFakeFlag bool // this is handled in common init excludedCommands = []string{"completion"} ) @@ -118,7 +118,8 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("config file (default is $%s/rcc.yaml)", common.Product.HomeVariable())) rootCmd.PersistentFlags().StringVar(&anythingIgnore, "anything", "", "freeform string value that can be set without any effect, for example CLI versioning/reference") - rootCmd.PersistentFlags().BoolVarP(&sema4FakeFlag, "sema4ai", "", false, "Select Sema4.ai toolset strategy.") + rootCmd.PersistentFlags().BoolVarP(&productFakeFlag, "sema4ai", "", false, "Select Sema4.ai toolset strategy.") + rootCmd.PersistentFlags().BoolVarP(&productFakeFlag, "robocorp", "", false, "Select Robocorp toolset strategy.") rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib (also RCC_NO_BUILD=1)") rootCmd.PersistentFlags().BoolVarP(&common.NoRetryBuild, "no-retry-build", "", false, "no retry in case of first environment build fails, just report error immediately") rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output (also RCC_VERBOSITY=silent)") diff --git a/common/platform_windows.go b/common/platform_windows.go index 05977132..a49322d1 100644 --- a/common/platform_windows.go +++ b/common/platform_windows.go @@ -11,10 +11,10 @@ import ( const ( defaultRobocorpLocation = "%LOCALAPPDATA%\\robocorp" - defaultHoloLocation = "c:\\ProgramData\\robocorp\\ht" + defaultHoloLocation = "%ProgramData%\\robocorp\\ht" defaultSema4Location = "%LOCALAPPDATA%\\sema4ai" - defaultSema4HoloLocation = "c:\\ProgramData\\sema4ai\\ht" + defaultSema4HoloLocation = "%ProgramData%\\sema4ai\\ht" ) var ( diff --git a/common/variables.go b/common/variables.go index 8a2d91b1..8caf3f59 100644 --- a/common/variables.go +++ b/common/variables.go @@ -81,9 +81,17 @@ func init() { args := set.Set(lowargs) WarrantyVoidedFlag = set.Member(args, "--warranty-voided") BundledFlag = set.Member(args, "--bundled") - if set.Member(args, "--sema4ai") { + sema4ai := set.Member(args, "--sema4ai") + robocorp := set.Member(args, "--robocorp") + switch { + case sema4ai && robocorp: + fmt.Fprintln(os.Stderr, "Fatal: rcc cannot be on both --robocorp and --sema4ai product modes at same time! Just use one of those, not both!") + os.Exit(99) + case sema4ai: Product = Sema4Mode() - } else { + case robocorp: + Product = LegacyMode() + default: Product = LegacyMode() } NoTempManagement = set.Member(args, "--no-temp-management") diff --git a/common/version.go b/common/version.go index eda978d6..31744ede 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.3` + Version = `v18.0.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6168a6ae..1fe7280b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.0.4 (date: 12.6.2024) WORK IN PROGRESS + +- Additional `--robocorp` product flag added. To match `--sema4ai` flag. +- Now using `%ProgramData%` instead of hard coded `c:\ProgramData\` in code. +- Update on default `settings.yaml` for Sema4.ai products. + ## v18.0.3 (date: 7.6.2024) WORK IN PROGRESS - Windows bugfix: icacls now applied on shared holotree location from product diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot index 2b820d07..91b8e153 100644 --- a/robot_tests/sema4ai.robot +++ b/robot_tests/sema4ai.robot @@ -20,9 +20,11 @@ Sema4.ai teardown Goal: See rcc toplevel help for Sema4.ai Step build/rcc --sema4ai --controller citests --help Must Have SEMA4AI + Must Have Robocorp + Must Have --robocorp + Must Have --sema4ai Must Have completion Wont Have ROBOCORP - Wont Have Robocorp Wont Have Robot Wont Have robot Wont Have bash From 707a5c7bfe27d1487b882325c936f44fc8119f83 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 14 Jun 2024 11:40:29 +0300 Subject: [PATCH 507/516] Change: --defaults flag in settings (v18.0.5) - MAJOR breaking change: now command `rcc configuration settings` will require `--defaults` flag to show defaults template. Without it, default functionality now is to show effective/active settings in YAML format. --- cmd/settings.go | 27 +++++++++++++++++++-------- common/version.go | 2 +- docs/changelog.md | 6 ++++++ robot_tests/sema4ai.robot | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/cmd/settings.go b/cmd/settings.go index 2eade0bd..4cfe2950 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -9,27 +9,38 @@ import ( "github.com/spf13/cobra" ) +var ( + settingsDefaults bool +) + var settingsCmd = &cobra.Command{ Use: "settings", - Short: "Show DEFAULT settings.yaml content. Vanilla rcc settings.", - Long: `Show DEFAULT settings.yaml content. Vanilla rcc settings. -If you need active status, either use --json option, or "rcc configuration diagnostics".`, + Short: "Show effective settings.yaml content.", + Long: `Show effective/active settings.yaml content. If you need DEFAULT status, use --defaults option.`, Run: func(cmd *cobra.Command, args []string) { - if jsonFlag { + switch { + case settingsDefaults: + raw, err := settings.DefaultSettings() + pretty.Guard(err == nil, 1, "Error while loading defaults: %v", err) + fmt.Fprintf(os.Stdout, "%s", string(raw)) + case jsonFlag: config, err := settings.SummonSettings() pretty.Guard(err == nil, 2, "Error while loading settings: %v", err) json, err := config.AsJson() pretty.Guard(err == nil, 3, "Error while converting settings: %v", err) fmt.Fprintf(os.Stdout, "%s", string(json)) - } else { - raw, err := settings.DefaultSettings() - pretty.Guard(err == nil, 1, "Error while loading defaults: %v", err) - fmt.Fprintf(os.Stdout, "%s", string(raw)) + default: + config, err := settings.SummonSettings() + pretty.Guard(err == nil, 2, "Error while loading settings: %v", err) + yaml, err := config.AsYaml() + pretty.Guard(err == nil, 3, "Error while converting settings: %v", err) + fmt.Fprintf(os.Stdout, "%s", string(yaml)) } }, } func init() { configureCmd.AddCommand(settingsCmd) + settingsCmd.Flags().BoolVarP(&settingsDefaults, "defaults", "d", false, "Show DEFAULT settings. Can be used as configuration template.") settingsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show EFFECTIVE settings as JSON stream. For applications to use.") } diff --git a/common/version.go b/common/version.go index 31744ede..760c2911 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.4` + Version = `v18.0.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1fe7280b..9520ddc7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.0.5 (date: 14.6.2024) WORK IN PROGRESS + +- MAJOR breaking change: now command `rcc configuration settings` will require + `--defaults` flag to show defaults template. Without it, default functionality + now is to show effective/active settings in YAML format. + ## v18.0.4 (date: 12.6.2024) WORK IN PROGRESS - Additional `--robocorp` product flag added. To match `--sema4ai` flag. diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot index 91b8e153..dda6dd12 100644 --- a/robot_tests/sema4ai.robot +++ b/robot_tests/sema4ai.robot @@ -43,7 +43,7 @@ Goal: See rcc commands for Sema4.ai Wont Have fish Goal: Default settings.yaml for Sema4.ai - Step build/rcc --sema4ai configuration settings --controller citests + Step build/rcc --sema4ai configuration settings --defaults --controller citests Must Have Sema4.ai default settings.yaml Wont Have assistant Wont Have branding From b8e35d271e3565a2dc846212882ef192bc6d4b66 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 26 Jun 2024 12:05:55 +0300 Subject: [PATCH 508/516] Feature: batch metrics (v18.1.0) - new command `feedback batch` for applications to send many metrics at once - disabling rcc internal metrics based on product strategy - bug fix: journal appending as more atomic operation (just one write) --- assets/sema4ai_settings.yaml | 2 +- cloud/metrics.go | 94 +++++++++++++++++++++++++++++++++++- cmd/assistantRun.go | 10 ++-- cmd/carrier.go | 2 +- cmd/feedback.go | 91 ++++++++++++++++++++++++++++++++++ cmd/issue.go | 50 ------------------- cmd/metric.go | 44 ----------------- cmd/push.go | 2 +- cmd/rcc/main.go | 6 +-- cmd/run.go | 2 +- cmd/testrun.go | 2 +- common/strategies.go | 9 ++++ common/version.go | 2 +- conda/installing.go | 2 +- conda/workflows.go | 16 +++--- docs/changelog.md | 6 +++ htfs/library.go | 2 +- journal/buildstats.go | 2 +- journal/journal.go | 10 ++-- operations/authorize.go | 2 +- operations/credentials.go | 2 +- operations/issues.go | 2 +- operations/running.go | 2 +- 23 files changed, 232 insertions(+), 130 deletions(-) delete mode 100644 cmd/issue.go delete mode 100644 cmd/metric.go diff --git a/assets/sema4ai_settings.yaml b/assets/sema4ai_settings.yaml index 5c8a2ebf..7adbbfb0 100644 --- a/assets/sema4ai_settings.yaml +++ b/assets/sema4ai_settings.yaml @@ -4,7 +4,7 @@ endpoints: cloud-ui: https://cloud.robocorp.com/ downloads: https://downloads.robocorp.com/ docs: https://robocorp.com/docs/ - telemetry: https://telemetry.robocorp.com/ + telemetry: https://sema4.ai/api/telemetry issues: https://telemetry.robocorp.com/ diagnostics-hosts: diff --git a/cloud/metrics.go b/cloud/metrics.go index 412a59cb..057903a1 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -1,13 +1,17 @@ package cloud import ( + "bytes" + "encoding/json" "fmt" "net/url" + "os" "runtime" "sync" "time" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -17,7 +21,20 @@ var ( ) const ( - trackingUrl = `/metric-v1/%v/%v/%v/%v/%v` + trackingUrl = `/metric-v1/%v/%v/%v/%v/%v` + batchUrl = `/metric-v1/batch` + contentType = `content-type` + applicationJson = `application/json` +) + +type ( + batchStatus struct { + File string `json:"file"` + Host string `json:"host"` + Code int `json:"code"` + Warning string `json:"warning"` + Content string `json:"content"` + } ) func sendMetric(metricsHost, kind, name, value string) { @@ -58,6 +75,81 @@ func BackgroundMetric(kind, name, value string) { } } +func InternalBackgroundMetric(kind, name, value string) { + if common.Product.AllowInternalMetrics() { + BackgroundMetric(kind, name, value) + } +} + +func stdoutDump(origin error, message any) (err error) { + defer fail.Around(&err) + + body, failure := json.MarshalIndent(message, "", " ") + fail.Fast(failure) + + os.Stdout.Write(append(body, '\n')) + + return origin +} + +func BatchMetric(filename string) error { + status := &batchStatus{ + File: filename, + Code: 999, + } + + metricsHost := settings.Global.TelemetryURL() + if len(metricsHost) < 8 { + status.Warning = "No metrics host." + return stdoutDump(nil, status) + } + + blob, err := os.ReadFile(filename) + if err != nil { + status.Code = 998 + status.Warning = err.Error() + return stdoutDump(err, status) + } + + status.Host = metricsHost + + client, err := NewClient(metricsHost) + if err != nil { + status.Code = 997 + status.Warning = err.Error() + return stdoutDump(err, status) + } + + timeout := 10 * time.Second + client = client.Uncritical().WithTimeout(timeout) + request := client.NewRequest(batchUrl) + request.Headers[contentType] = applicationJson + request.Body = bytes.NewBuffer([]byte(blob)) + response := client.Put(request) + switch { + case response == nil: + status.Code = 996 + status.Warning = "Response was " + case response != nil && response.Status == 202 && response.Err == nil: + status.Code = response.Status + status.Warning = "ok" + status.Content = string(response.Body) + case response != nil && response.Err != nil: + status.Code = response.Status + status.Warning = fmt.Sprintf("Failed PUT to %s%s, reason: %v", metricsHost, batchUrl, response.Err) + status.Content = string(response.Body) + case response != nil && response.Status != 202: + status.Code = response.Status + status.Warning = fmt.Sprintf("Failed PUT to %s%s. See content for details.", metricsHost, batchUrl) + status.Content = string(response.Body) + default: + status.Code = response.Status + status.Warning = "N/A" + status.Content = string(response.Body) + } + return stdoutDump(nil, status) +} + func WaitTelemetry() { defer common.Timeline("wait telemetry done") diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index e5735014..dcb711c5 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -44,9 +44,9 @@ var assistantRunCmd = &cobra.Command{ } common.Timeline("new cloud client created") reason = "START_FAILURE" - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.Elapsed().String()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.Elapsed().String()) defer func() { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.stop", reason) }() common.Timeline("start assistant run cloud call started") assistant, err := operations.StartAssistantRun(client, account, workspaceId, assistantId) @@ -90,7 +90,7 @@ var assistantRunCmd = &cobra.Command{ } defer func() { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.Elapsed().String()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.Elapsed().String()) }() defer func() { if len(assistant.ArtifactURL) == 0 { @@ -112,9 +112,9 @@ var assistantRunCmd = &cobra.Command{ } }() - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.setup", elapser.Elapsed().String()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.setup", elapser.Elapsed().String()) defer func() { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.Elapsed().String()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.executed", elapser.Elapsed().String()) }() reason = "ROBOT_FAILURE" operations.SelectExecutionModel(captureRunFlags(true), simple, todo.Commandline(), config, todo, label, false, assistant.Environment) diff --git a/cmd/carrier.go b/cmd/carrier.go index f3d5461e..78dac85b 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -63,7 +63,7 @@ func runCarrier() error { targetRobot := robot.DetectConfigurationName(workarea) simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) operations.SelectExecutionModel(captureRunFlags(false), simple, todo.Commandline(), config, todo, label, false, nil) return nil } diff --git a/cmd/feedback.go b/cmd/feedback.go index 2ca8f184..a907e50e 100644 --- a/cmd/feedback.go +++ b/cmd/feedback.go @@ -1,9 +1,26 @@ package cmd import ( + "fmt" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" ) +var ( + issueRobot string + issueMetafile string + issueAttachments []string + + metricType string + metricName string + metricValue string +) + var feedbackCmd = &cobra.Command{ Use: "feedback", Aliases: []string{"f"}, @@ -12,6 +29,80 @@ var feedbackCmd = &cobra.Command{ Hidden: true, } +var issueCmd = &cobra.Command{ + Use: "issue", + Short: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), + Long: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Feedback issue lasted").Report() + } + accountEmail := "unknown" + account := operations.AccountByName(AccountName()) + if account != nil && account.Details != nil { + email, ok := account.Details["email"].(string) + if ok { + accountEmail = email + } + } + err := operations.ReportIssue(accountEmail, issueRobot, issueMetafile, issueAttachments, dryFlag) + if err != nil { + pretty.Exit(1, "Error: %s", err) + } + pretty.Exit(0, "OK") + }, +} + +var metricCmd = &cobra.Command{ + Use: "metric", + Short: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), + Long: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Feedback metric lasted").Report() + } + if !xviper.CanTrack() { + pretty.Exit(1, "Tracking is disabled. Quitting.") + } + cloud.BackgroundMetric(metricType, metricName, metricValue) + pretty.Exit(0, "OK") + }, +} + +var batchMetricCmd = &cobra.Command{ + Use: "batch ", + Short: fmt.Sprintf("Send batch metrics to %s Control Room. For applications only.", common.Product.Name()), + Long: fmt.Sprintf("Send batch metrics to %s Control Room. For applications only.", common.Product.Name()), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Feedback batch lasted").Report() + } + if xviper.CanTrack() { + cloud.BatchMetric(args[0]) + } else { + pretty.Warning("Tracking is disabled. Quitting.") + } + }, +} + func init() { rootCmd.AddCommand(feedbackCmd) + + feedbackCmd.AddCommand(issueCmd) + issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") + issueCmd.MarkFlagRequired("report") + issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") + issueCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't send issue report, just show what would report be.") + issueCmd.Flags().StringVarP(&issueRobot, "robot", "", "", "Full path to 'robot.yaml' configuration file. [optional]") + + feedbackCmd.AddCommand(metricCmd) + metricCmd.Flags().StringVarP(&metricType, "type", "t", "", "Type for metric source to use.") + metricCmd.MarkFlagRequired("type") + metricCmd.Flags().StringVarP(&metricName, "name", "n", "", "Name for metric to report.") + metricCmd.MarkFlagRequired("name") + metricCmd.Flags().StringVarP(&metricValue, "value", "v", "", "Value for metric to report.") + metricCmd.MarkFlagRequired("value") + + feedbackCmd.AddCommand(batchMetricCmd) } diff --git a/cmd/issue.go b/cmd/issue.go deleted file mode 100644 index 47f4d273..00000000 --- a/cmd/issue.go +++ /dev/null @@ -1,50 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - issueRobot string - issueMetafile string - issueAttachments []string -) - -var issueCmd = &cobra.Command{ - Use: "issue", - Short: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), - Long: fmt.Sprintf("Send an issue to %s Control Room via rcc.", common.Product.Name()), - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Feedback issue lasted").Report() - } - accountEmail := "unknown" - account := operations.AccountByName(AccountName()) - if account != nil && account.Details != nil { - email, ok := account.Details["email"].(string) - if ok { - accountEmail = email - } - } - err := operations.ReportIssue(accountEmail, issueRobot, issueMetafile, issueAttachments, dryFlag) - if err != nil { - pretty.Exit(1, "Error: %s", err) - } - pretty.Exit(0, "OK") - }, -} - -func init() { - feedbackCmd.AddCommand(issueCmd) - issueCmd.Flags().StringVarP(&issueMetafile, "report", "r", "", "Report file in JSON form containing actual issue report details.") - issueCmd.MarkFlagRequired("report") - issueCmd.Flags().StringArrayVarP(&issueAttachments, "attachments", "a", []string{}, "Files to attach to issue report.") - issueCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't send issue report, just show what would report be.") - issueCmd.Flags().StringVarP(&issueRobot, "robot", "", "", "Full path to 'robot.yaml' configuration file. [optional]") -} diff --git a/cmd/metric.go b/cmd/metric.go deleted file mode 100644 index 7366028c..00000000 --- a/cmd/metric.go +++ /dev/null @@ -1,44 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/robocorp/rcc/cloud" - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/xviper" - - "github.com/spf13/cobra" -) - -var ( - metricType string - metricName string - metricValue string -) - -var metricCmd = &cobra.Command{ - Use: "metric", - Short: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), - Long: fmt.Sprintf("Send some metric to %s Control Room.", common.Product.Name()), - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Feedback metric lasted").Report() - } - if !xviper.CanTrack() { - pretty.Exit(1, "Tracking is disabled. Quitting.") - } - cloud.BackgroundMetric(metricType, metricName, metricValue) - pretty.Exit(0, "OK") - }, -} - -func init() { - feedbackCmd.AddCommand(metricCmd) - metricCmd.Flags().StringVarP(&metricType, "type", "t", "", "Type for metric source to use.") - metricCmd.MarkFlagRequired("type") - metricCmd.Flags().StringVarP(&metricName, "name", "n", "", "Name for metric to report.") - metricCmd.MarkFlagRequired("name") - metricCmd.Flags().StringVarP(&metricValue, "value", "v", "", "Value for metric to report.") - metricCmd.MarkFlagRequired("value") -} diff --git a/cmd/push.go b/cmd/push.go index 6421098a..6dcc227d 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -43,7 +43,7 @@ var pushCmd = &cobra.Command{ if err != nil { pretty.Exit(4, "Error: %v", err) } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.push", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.cli.push", common.Version) pretty.Ok() }, } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index d977fcf9..96c9b7a7 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -64,8 +64,8 @@ func TimezoneMetric() error { } cache.Stamps[timezonekey] = common.When + daily zone := time.Now().Format("MST-0700") - cloud.BackgroundMetric(common.ControllerIdentity(), timezonekey, zone) - cloud.BackgroundMetric(common.ControllerIdentity(), oskey, common.Platform()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), timezonekey, zone) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), oskey, common.Platform()) return cache.Save() } @@ -82,7 +82,7 @@ func ExitProtection() { common.WaitLogs() os.Exit(exit.Code) } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.panic.origin", cmd.Origin()) cloud.WaitTelemetry() common.WaitLogs() panic(status) diff --git a/cmd/run.go b/cmd/run.go index 567156c6..949b0369 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -30,7 +30,7 @@ in your own machine.`, defer common.Stopwatch("Task run lasted").Report() } simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) commandline := todo.Commandline() commandline = append(commandline, args...) operations.SelectExecutionModel(captureRunFlags(false), simple, commandline, config, todo, label, interactiveFlag, nil) diff --git a/cmd/testrun.go b/cmd/testrun.go index 0bf124a0..11cb1d70 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -53,7 +53,7 @@ var testrunCmd = &cobra.Command{ targetRobot := robot.DetectConfigurationName(workarea) simple, config, todo, label := operations.LoadTaskWithEnvironment(targetRobot, runTask, forceFlag) defer common.Log("Moving outputs to %v directory.", testrunDir) - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.cli.testrun", common.Version) commandline := todo.Commandline() commandline = append(commandline, args...) operations.SelectExecutionModel(captureRunFlags(false), simple, commandline, config, todo, label, false, nil) diff --git a/common/strategies.go b/common/strategies.go index c97bcad3..cf7ffebf 100644 --- a/common/strategies.go +++ b/common/strategies.go @@ -18,6 +18,7 @@ type ( Home() string HoloLocation() string DefaultSettingsYamlFile() string + AllowInternalMetrics() bool } legacyStrategy struct { @@ -45,6 +46,10 @@ func (it *legacyStrategy) IsLegacy() bool { return true } +func (it *legacyStrategy) AllowInternalMetrics() bool { + return true +} + func (it *legacyStrategy) ForceHome(value string) { it.forcedHome = value } @@ -80,6 +85,10 @@ func (it *sema4Strategy) IsLegacy() bool { return false } +func (it *sema4Strategy) AllowInternalMetrics() bool { + return !IsBundled() +} + func (it *sema4Strategy) ForceHome(value string) { it.forcedHome = value } diff --git a/common/version.go b/common/version.go index 760c2911..a3d73441 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.0.5` + Version = `v18.1.0` ) diff --git a/conda/installing.go b/conda/installing.go index 261e83e5..16ac9613 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -62,7 +62,7 @@ func DoExtract(delay time.Duration) bool { common.Fatal("Could not make micromamba executalbe, reason:", err) return false } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.extract", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.extract", common.Version) common.PlatformSyncDelay() return true } diff --git a/conda/workflows.go b/conda/workflows.go index 44fd5b27..70e3fb74 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -118,7 +118,7 @@ func (it InstallObserver) Write(content []byte) (int, error) { func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) renameRemove(targetFolder) location := filepath.Join(common.Product.Home(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) @@ -146,7 +146,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, skip, finalEnv, recorder) if !success && !force && !fatal && !common.NoRetryBuild { journal.CurrentBuildEvent().Rebuild() - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) common.Debug("=== second try phase ===") common.Timeline("second try.") common.ForceDebug() @@ -194,7 +194,7 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. tee := io.MultiWriter(observer, planWriter) code, err := shell.New(CondaEnvironment(), ".", mambaCommand.CLI()...).Tracked(tee, false) if err != nil || code != 0 { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.micromamba", fmt.Sprintf("%d_%x", code, code)) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.micromamba", fmt.Sprintf("%d_%x", code, code)) common.Timeline("micromamba fail.") common.Fatal(fmt.Sprintf("Micromamba [%d/%x]", code, code), err) pretty.RccPointOfView(micromambaInstall, err) @@ -239,7 +239,7 @@ func uvLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.S common.Debug("=== uv install phase ===") code, err := LiveExecution(planWriter, targetFolder, uvCommand.CLI()...) if err != nil || code != 0 { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.uv", fmt.Sprintf("%d_%x", code, code)) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.uv", fmt.Sprintf("%d_%x", code, code)) common.Timeline("uv fail.") common.Fatal(fmt.Sprintf("uv [%d/%x]", code, code), err) pretty.RccPointOfView(uvInstall, err) @@ -269,7 +269,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. pretty.Progress(8, "Skipping pip install phase -- no pip dependencies.") } else { if !pyok { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) common.Timeline("pip fail. no python found.") common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) return false, false, pipUsed, "" @@ -283,7 +283,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. common.Debug("=== pip install phase ===") code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil || code != 0 { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") common.Fatal(fmt.Sprintf("Pip [%d/%x]", code, code), err) pretty.RccPointOfView(pipInstall, err) @@ -438,7 +438,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== pip check phase ===") code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) if err != nil || code != 0 { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip check fail.") common.Fatal(fmt.Sprintf("Pip check [%d/%x]", code, code), err) return false, false @@ -497,7 +497,7 @@ func temporaryConfig(condaYaml, requirementsText, filename string) (string, stri } func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configuration string) error { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.ProductLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [robocorp lock]") diff --git a/docs/changelog.md b/docs/changelog.md index 9520ddc7..d44261f5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.1.0 (date: 26.6.2024) WORK IN PROGRESS + +- new command `feedback batch` for applications to send many metrics at once +- disabling rcc internal metrics based on product strategy +- bug fix: journal appending as more atomic operation (just one write) + ## v18.0.5 (date: 14.6.2024) WORK IN PROGRESS - MAJOR breaking change: now command `rcc configuration settings` will require diff --git a/htfs/library.go b/htfs/library.go index 9829e446..49c41713 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -323,7 +323,7 @@ func (it *hololib) queryBlueprint(key string) bool { err = shadow.Treetop(CatalogCheck(it, shadow)) common.TimelineEnd() if err != nil { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.holotree.catalog.failure", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.holotree.catalog.failure", common.Version) common.Debug("Catalog check failed, reason: %v", err) return false } diff --git a/journal/buildstats.go b/journal/buildstats.go index b3d8291d..6c30228c 100644 --- a/journal/buildstats.go +++ b/journal/buildstats.go @@ -402,7 +402,7 @@ func serialize(event *BuildEvent) (err error) { blob, err := json.Marshal(event) fail.On(err != nil, "Could not serialize event: %v -> %v", event.What, err) - return appendJournal(CurrentEventFilename(), blob) + return AppendJournal(CurrentEventFilename(), blob) } func NewBuildEvent() *BuildEvent { diff --git a/journal/journal.go b/journal/journal.go index fe0a4564..929a19cf 100644 --- a/journal/journal.go +++ b/journal/journal.go @@ -65,7 +65,7 @@ func (it *journal) Post(event, detail, commentForm string, fields ...interface{} } blob, err := json.Marshal(message) fail.On(err != nil, "Could not serialize event: %v -> %v", event, err) - return appendJournal(it.filename, blob) + return AppendJournal(it.filename, blob) } func Unify(value string) string { @@ -83,10 +83,10 @@ func Post(event, detail, commentForm string, fields ...interface{}) (err error) } blob, err := json.Marshal(message) fail.On(err != nil, "Could not serialize event: %v -> %v", event, err) - return appendJournal(common.EventJournal(), blob) + return AppendJournal(common.EventJournal(), blob) } -func appendJournal(journalname string, blob []byte) (err error) { +func AppendJournal(journalname string, blob []byte) (err error) { defer fail.Around(&err) if common.WarrantyVoided() { return nil @@ -94,9 +94,7 @@ func appendJournal(journalname string, blob []byte) (err error) { handle, err := os.OpenFile(journalname, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) defer handle.Close() - _, err = handle.Write(blob) - fail.On(err != nil, "Failed to write event journal %v -> %v", journalname, err) - _, err = handle.Write([]byte{'\n'}) + _, err = handle.Write(append(blob, '\n')) fail.On(err != nil, "Failed to write event journal %v -> %v", journalname, err) return handle.Sync() } diff --git a/operations/authorize.go b/operations/authorize.go index 0bd362f6..848652f1 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -108,7 +108,7 @@ func RunAssistantClaims(seconds int, workspace string) *Claims { func RunRobotClaims(seconds int, workspace string) *Claims { result := NewClaims("RunRobot", fmt.Sprintf(WorkspaceApi, workspace), seconds) result.CapabilitySet = "run/robot" - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.capabilityset.runrobot", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.capabilityset.runrobot", common.Version) return result } diff --git a/operations/credentials.go b/operations/credentials.go index 59138199..66e138c2 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -216,7 +216,7 @@ func loadAccount(label string) *account { } func createEphemeralAccount(parts []string) *account { - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.account.ephemeral", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.account.ephemeral", common.Version) common.NoCache = true endpoint := settings.Global.DefaultEndpoint() if len(parts[3]) > 0 { diff --git a/operations/issues.go b/operations/issues.go index 0c512d62..39bf80e9 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -77,7 +77,7 @@ func ReportIssue(email, robotFile, reportFile string, attachmentsFiles []string, if len(issueHost) == 0 { return nil } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.submit.issue", common.Version) token, err := loadToken(reportFile) if err != nil { return err diff --git a/operations/running.go b/operations/running.go index 02c227fd..413ba80b 100644 --- a/operations/running.go +++ b/operations/running.go @@ -372,7 +372,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } if exitcode != 0 { details := fmt.Sprintf("%s_%d_%08x", common.Platform(), exitcode, uint32(exitcode)) - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run.failure", details) + cloud.InternalBackgroundMetric(common.ControllerIdentity(), "rcc.cli.run.failure", details) } }) pretty.RccPointOfView(actualRun, err) From bd2176af8c6873142877432e2b259df2ea9546e5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 26 Jun 2024 12:55:08 +0300 Subject: [PATCH 509/516] Bugfix: too many commands were hidden (v18.1.1) - bug fix: too many commands were only visible with `--robocorp` product strategy, but they are needed also in `--sema4ai` strategy --- cmd/cloudNew.go | 14 ++++++-------- cmd/cloudPrepare.go | 14 ++++++-------- cmd/download.go | 14 ++++++-------- cmd/pull.go | 18 ++++++++---------- cmd/push.go | 16 +++++++--------- cmd/robot.go | 4 +--- cmd/run.go | 30 ++++++++++++++---------------- cmd/task.go | 6 ++---- cmd/upload.go | 14 ++++++-------- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot_tests/sema4ai.robot | 12 ++++++++++-- 12 files changed, 72 insertions(+), 77 deletions(-) diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index a8dd8af2..4dc615b5 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -44,12 +44,10 @@ var newCloudCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(newCloudCmd) - newCloudCmd.Flags().StringVarP(&robotName, "robot", "r", "", "Name for new robot to create.") - newCloudCmd.MarkFlagRequired("robot") - newCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use as creation target.") - newCloudCmd.MarkFlagRequired("workspace") - newCloudCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") - } + cloudCmd.AddCommand(newCloudCmd) + newCloudCmd.Flags().StringVarP(&robotName, "robot", "r", "", "Name for new robot to create.") + newCloudCmd.MarkFlagRequired("robot") + newCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use as creation target.") + newCloudCmd.MarkFlagRequired("workspace") + newCloudCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") } diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 7ff0761d..4737aa28 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -64,12 +64,10 @@ var prepareCloudCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(prepareCloudCmd) - prepareCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - prepareCloudCmd.MarkFlagRequired("workspace") - prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - prepareCloudCmd.MarkFlagRequired("robot") - prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") - } + cloudCmd.AddCommand(prepareCloudCmd) + prepareCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("workspace") + prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + prepareCloudCmd.MarkFlagRequired("robot") + prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") } diff --git a/cmd/download.go b/cmd/download.go index 2395545c..5e3ad859 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -36,12 +36,10 @@ var downloadCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(downloadCmd) - downloadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the downloaded robot.") - downloadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - downloadCmd.MarkFlagRequired("workspace") - downloadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - downloadCmd.MarkFlagRequired("robot") - } + cloudCmd.AddCommand(downloadCmd) + downloadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the downloaded robot.") + downloadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + downloadCmd.MarkFlagRequired("workspace") + downloadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + downloadCmd.MarkFlagRequired("robot") } diff --git a/cmd/pull.go b/cmd/pull.go index f69538b4..e7cc800f 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -52,14 +52,12 @@ var pullCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(pullCmd) - pullCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") - pullCmd.MarkFlagRequired("workspace") - pullCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") - pullCmd.MarkFlagRequired("robot") - pullCmd.Flags().StringVarP(&directory, "directory", "d", "", "The root directory to extract the robot into.") - pullCmd.MarkFlagRequired("directory") - pullCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Remove safety nets around the unwrapping of the robot.") - } + cloudCmd.AddCommand(pullCmd) + pullCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the download source.") + pullCmd.MarkFlagRequired("workspace") + pullCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") + pullCmd.MarkFlagRequired("robot") + pullCmd.Flags().StringVarP(&directory, "directory", "d", "", "The root directory to extract the robot into.") + pullCmd.MarkFlagRequired("directory") + pullCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Remove safety nets around the unwrapping of the robot.") } diff --git a/cmd/push.go b/cmd/push.go index 6dcc227d..798a9052 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -49,13 +49,11 @@ var pushCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(pushCmd) - pushCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to create the robot from.") - pushCmd.Flags().StringArrayVarP(&ignores, "ignore", "i", []string{}, "Files containing ignore patterns.") - pushCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") - pushCmd.MarkFlagRequired("workspace") - pushCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") - pushCmd.MarkFlagRequired("robot") - } + cloudCmd.AddCommand(pushCmd) + pushCmd.Flags().StringVarP(&directory, "directory", "d", ".", "The root directory to create the robot from.") + pushCmd.Flags().StringArrayVarP(&ignores, "ignore", "i", []string{}, "Files containing ignore patterns.") + pushCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") + pushCmd.MarkFlagRequired("workspace") + pushCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") + pushCmd.MarkFlagRequired("robot") } diff --git a/cmd/robot.go b/cmd/robot.go index db7befc5..feef996d 100644 --- a/cmd/robot.go +++ b/cmd/robot.go @@ -16,7 +16,5 @@ executed either locally, or in connection to %s Control Room and tooling.`, comm } func init() { - if common.Product.IsLegacy() { - rootCmd.AddCommand(robotCmd) - } + rootCmd.AddCommand(robotCmd) } diff --git a/cmd/run.go b/cmd/run.go index 949b0369..2c75e868 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -52,21 +52,19 @@ func captureRunFlags(assistant bool) *operations.RunFlags { } func init() { - if common.Product.IsLegacy() { - rootCmd.AddCommand(runCmd) - taskCmd.AddCommand(runCmd) + rootCmd.AddCommand(runCmd) + taskCmd.AddCommand(runCmd) - runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") - runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") - runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") - runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") - runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") - runCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") - runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") - runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") - runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") - runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") - runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") - runCmd.Flags().BoolVarP(&common.DeveloperFlag, "dev", "", false, "Use devTasks instead of normal tasks. For development work only. Stragegy selection.") - } + runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") + runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") + runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") + runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") + runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + runCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") + runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") + runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") + runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") + runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") + runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") + runCmd.Flags().BoolVarP(&common.DeveloperFlag, "dev", "", false, "Use devTasks instead of normal tasks. For development work only. Stragegy selection.") } diff --git a/cmd/task.go b/cmd/task.go index bfc102cc..8d097f9b 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -16,9 +16,7 @@ executed either locally, or in connection to %s Control Room and tooling.`, comm } func init() { - if common.Product.IsLegacy() { - rootCmd.AddCommand(taskCmd) + rootCmd.AddCommand(taskCmd) - taskCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") - } + taskCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") } diff --git a/cmd/upload.go b/cmd/upload.go index be964269..9b361c30 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -36,12 +36,10 @@ var uploadCmd = &cobra.Command{ } func init() { - if common.Product.IsLegacy() { - cloudCmd.AddCommand(uploadCmd) - uploadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the robot.") - uploadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") - uploadCmd.MarkFlagRequired("workspace") - uploadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") - uploadCmd.MarkFlagRequired("robot") - } + cloudCmd.AddCommand(uploadCmd) + uploadCmd.Flags().StringVarP(&zipfile, "zipfile", "z", "robot.zip", "The filename for the robot.") + uploadCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "The workspace id to use as the upload target.") + uploadCmd.MarkFlagRequired("workspace") + uploadCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the upload target.") + uploadCmd.MarkFlagRequired("robot") } diff --git a/common/version.go b/common/version.go index a3d73441..57ff773c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.0` + Version = `v18.1.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index d44261f5..346cdaf5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v18.1.1 (date: 26.6.2024) WORK IN PROGRESS + +- bug fix: too many commands were only visible with `--robocorp` product + strategy, but they are needed also in `--sema4ai` strategy + ## v18.1.0 (date: 26.6.2024) WORK IN PROGRESS - new command `feedback batch` for applications to send many metrics at once diff --git a/robot_tests/sema4ai.robot b/robot_tests/sema4ai.robot index dda6dd12..7624f3a2 100644 --- a/robot_tests/sema4ai.robot +++ b/robot_tests/sema4ai.robot @@ -24,9 +24,13 @@ Goal: See rcc toplevel help for Sema4.ai Must Have --robocorp Must Have --sema4ai Must Have completion + Must Have robot Wont Have ROBOCORP Wont Have Robot - Wont Have robot + Wont Have assistant + Wont Have interactive + Wont Have community + Wont Have tutorial Wont Have bash Wont Have fish @@ -37,7 +41,11 @@ Goal: See rcc commands for Sema4.ai Wont Have ROBOCORP Wont Have Robocorp Wont Have Robot - Wont Have robot + Must Have robot + Wont Have assistant + Wont Have interactive + Wont Have community + Wont Have tutorial Wont Have completion Wont Have bash Wont Have fish From a567e5f464de8fcb575404853d3843fa4f5116b4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 28 Jun 2024 09:32:55 +0300 Subject: [PATCH 510/516] Bugfix: Sema4.ai settings.yaml defaults (v18.1.2) - updated default settings.yaml for Sema4.ai products. - templates location also back in Sema4.ai settings.yaml. - documentation updates --- assets/sema4ai_settings.yaml | 7 +++++-- common/version.go | 2 +- docs/README.md | 20 +++++++++++--------- docs/changelog.md | 6 ++++++ docs/features.md | 2 ++ docs/maintenance.md | 7 +++++++ docs/vocabulary.md | 4 ++++ 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/assets/sema4ai_settings.yaml b/assets/sema4ai_settings.yaml index 7adbbfb0..448738a6 100644 --- a/assets/sema4ai_settings.yaml +++ b/assets/sema4ai_settings.yaml @@ -2,8 +2,8 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ cloud-linking: https://cloud.robocorp.com/link/ cloud-ui: https://cloud.robocorp.com/ - downloads: https://downloads.robocorp.com/ - docs: https://robocorp.com/docs/ + downloads: https://cdn.sema4.ai/ + docs: https://sema4.ai/docs/ telemetry: https://sema4.ai/api/telemetry issues: https://telemetry.robocorp.com/ @@ -13,6 +13,9 @@ diagnostics-hosts: - conda.anaconda.org - pypi.org +autoupdates: + templates: https://cdn.sema4.ai/templates/templates.yaml + certificates: verify-ssl: true ssl-no-revoke: false diff --git a/common/version.go b/common/version.go index 57ff773c..b31c3e48 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.1` + Version = `v18.1.2` ) diff --git a/docs/README.md b/docs/README.md index 2e5cd926..b871113a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -92,9 +92,10 @@ ### 5.1 [Why do maintenance?](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#why-do-maintenance) ### 5.2 [Shared holotree and maintenance](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#shared-holotree-and-maintenance) ### 5.3 [Maintenance vs. tools using holotrees](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#maintenance-vs-tools-using-holotrees) -### 5.4 [Deleting catalogs and spaces](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#deleting-catalogs-and-spaces) -### 5.5 [Keeping hololib consistent](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#keeping-hololib-consistent) -### 5.6 [Summary of maintenance related commands](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#summary-of-maintenance-related-commands) +### 5.4 [Maintenace and product families](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#maintenace-and-product-families) +### 5.5 [Deleting catalogs and spaces](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#deleting-catalogs-and-spaces) +### 5.6 [Keeping hololib consistent](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#keeping-hololib-consistent) +### 5.7 [Summary of maintenance related commands](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#summary-of-maintenance-related-commands) ## 6 [Support for virtual environments](https://github.com/robocorp/rcc/blob/master/docs/venv.md#support-for-virtual-environments) ### 6.1 [What does it do?](https://github.com/robocorp/rcc/blob/master/docs/venv.md#what-does-it-do) ### 6.2 [How to get started?](https://github.com/robocorp/rcc/blob/master/docs/venv.md#how-to-get-started) @@ -126,12 +127,13 @@ ### 8.12 [Prebuild environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#prebuild-environment) ### 8.13 [Pristine environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#pristine-environment) ### 8.14 [Private holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#private-holotree) -### 8.15 [Profile](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#profile) -### 8.16 [Robot](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#robot) -### 8.17 [Shared holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#shared-holotree) -### 8.18 [Space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#space) -### 8.19 [Unmanaged holotree space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#unmanaged-holotree-space) -### 8.20 [User](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#user) +### 8.15 [Product family](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#product-family) +### 8.16 [Profile](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#profile) +### 8.17 [Robot](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#robot) +### 8.18 [Shared holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#shared-holotree) +### 8.19 [Space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#space) +### 8.20 [Unmanaged holotree space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#unmanaged-holotree-space) +### 8.21 [User](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#user) ## 9 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) ### 9.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) ### 9.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) diff --git a/docs/changelog.md b/docs/changelog.md index 346cdaf5..1e18ba23 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.1.2 (date: 28.6.2024) + +- updated default settings.yaml for Sema4.ai products. +- templates location also back in Sema4.ai settings.yaml. +- documentation updates + ## v18.1.1 (date: 26.6.2024) WORK IN PROGRESS - bug fix: too many commands were only visible with `--robocorp` product diff --git a/docs/features.md b/docs/features.md index 3dee01d4..2fd63612 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,12 +1,14 @@ # Incomplete list of rcc features * supported operating systems are Windows, MacOS, and Linux +* support for product families `--robocorp` and `--sema4ai` * supported sources for environment building are both conda and pypi * provide repeatable, isolated, and clean environments for automations and robots to run on * automatic environment creation based on declarative conda environment.yaml files * easily run software robots (automations) based on declarative robot.yaml files +* also support environment creation from package.yaml files * test robots in isolated environments before uploading them to Control Room * provide commands for Robocorp runtime and developer tools (Worker, Assistant, VS Code, ...) diff --git a/docs/maintenance.md b/docs/maintenance.md index 7f2e2a5a..4233001b 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -38,6 +38,13 @@ Some of those effects might be: or tooling, might cause slowness when needed next time, or if builds are prevented it might even deny usage of those spaces +## Maintenace and product families + +Since v18 of rcc, there are two different product families present. To +explicitely maintain specific product family holotree, then either +`--robocorp` or `--sema4ai` flag should be given. Both product families +have their separate holotree libraries and spaces. + ## Deleting catalogs and spaces Before you delete anything, you should be aware of those things and what is diff --git a/docs/vocabulary.md b/docs/vocabulary.md index 3be77c64..72b7ed7c 100644 --- a/docs/vocabulary.md +++ b/docs/vocabulary.md @@ -94,6 +94,10 @@ This is state, where all environments are created for single user, and cannot be shared between users. These must be build and managed privately and using them normally requires internet access. +## Product family + +This is reference to either Robocorp products or Sema4.ai products. + ## Profile Profile is set of settings that describes network and Robocorp configurations From 7df54214080b742b162a451ed7fb6d8568dc185b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 2 Aug 2024 08:38:29 +0300 Subject: [PATCH 511/516] Bugfix and documentation updates (v18.1.3) - bugfix: tlsCheck was giving nil TLS information without error - CONTRIBUTING.md -- things to note when developing rcc --- CONTRIBUTING.md | 45 ++++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/tlscheck.go | 3 +++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d5e425d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# How to contribute + +## Ideas for regular sources of contribution. + +1. Is there a need to upgrade the Go language version? + - see releases from https://go.dev/doc/devel/release + - do not go to bleeding edge (unless you really have to do so) + - do not stay too far away behind development + - when needed, update .github/workflows/rcc.yaml +2. Is there new Micromamba available? + - see releases from https://anaconda.org/conda-forge/micromamba + - only use stable versions +3. Where is uv going? + - important: uv has to come from conda-forge because of enterprise + firewalls/proxies + - https://anaconda.org/conda-forge/uv to find out what is available + on conda-forge + - https://github.com/astral-sh/uv to see issues and development +4. What is pip doing? + - check news from https://pip.pypa.io/en/stable/news/ + +## Additional sources for contribution ideas. + +- improve documentation under docs/ directory +- improve acceptance tests written in Robot Framework (inside `robot_tests` + directory) + - currently these work fully on Linux only, so if you have Mac or Windows + and can make these work there, that would also be a nice contribution + +## How to proceed with improvements/contributions? + +- create an issue in the rcc repository at + https://github.com/robocorp/rcc/issues +- on that issue, discuss the solution you are proposing +- implementation can proceed only when the solution is clear and accepted +- the solution should be made so that it works on Mac, Windows, and Linux +- when developing, remember to run both unit tests and acceptance tests + (Robot Framework tests) on your own machine first +- once you have written the code for that solution, create a pull request + +## How does rcc build work? + +- a good source to understand the build is to see the CI pipeline, + .github/workflows/rcc.yaml +- also read docs/BUILD.md for tooling requirements and commands to run diff --git a/common/version.go b/common/version.go index b31c3e48..466b578c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.2` + Version = `v18.1.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1e18ba23..2fe7e41f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v18.1.3 (date: 2.8.2024) + +- bugfix: tlsCheck was giving nil TLS information without error +- CONTRIBUTING.md -- things to note when developing rcc + ## v18.1.2 (date: 28.6.2024) - updated default settings.yaml for Sema4.ai products. diff --git a/operations/tlscheck.go b/operations/tlscheck.go index 9e0973d4..bca275b7 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -70,6 +70,9 @@ func tlsCheckHeadOnly(url string) (*tls.ConnectionState, error) { if err != nil { return nil, err } + if response.TLS == nil { + return nil, fmt.Errorf("Strange state, could not get TLS information from URL %q and there was no error.", url) + } return response.TLS, nil } From 31cc64c3af87c94c1fe6556c115c508dc2a94182 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 5 Aug 2024 11:47:22 +0300 Subject: [PATCH 512/516] Bugfix: filtering PS1 out of environment (v18.1.4) --- .gitignore | 1 + cmd/holotreeVariables.go | 16 +++++++++++++++- common/version.go | 2 +- developer/.gitignore | 17 +++++++++++++++++ developer/README.md | 21 +++++++++++++++++++++ developer/builder.py | 4 ++++ developer/setup.yaml | 7 +++++++ developer/tester.py | 4 ++++ developer/toolkit.yaml | 15 +++++++++++++++ docs/changelog.md | 5 +++++ 10 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 developer/.gitignore create mode 100644 developer/README.md create mode 100644 developer/builder.py create mode 100644 developer/setup.yaml create mode 100644 developer/tester.py create mode 100644 developer/toolkit.yaml diff --git a/.gitignore b/.gitignore index 04877b44..27255eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ __pycache__/ build/ output/ tmp/ +developer/tmp/ rcc.yaml rcccache.yaml tags diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index fe75f4b1..4033c27f 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -129,7 +129,21 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp env = append(env, fmt.Sprintf("RC_WORKSPACE_ID=%s", workspaceId)) } - return env + return removeUnwanted(env) +} + +func removeUnwanted(variables []string) []string { + result := make([]string, 0, len(variables)) + for _, line := range variables { + switch { + case strings.HasPrefix(line, "PS1="): + continue + default: + result = append(result, line) + } + } + + return result } var holotreeVariablesCmd = &cobra.Command{ diff --git a/common/version.go b/common/version.go index 466b578c..f25a73cd 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.3` + Version = `v18.1.4` ) diff --git a/developer/.gitignore b/developer/.gitignore new file mode 100644 index 00000000..811ab3fe --- /dev/null +++ b/developer/.gitignore @@ -0,0 +1,17 @@ +output/ +venv/ +temp/ +.rpa/ +.idea/ +.ipynb_checkpoints/ +*/.ipynb_checkpoints/* +.virtual_documents/ +*/.virtual_documents/* +.vscode +.DS_Store +*.pyc +*.zip +*/work-items-out/* +testrun/* +.~lock* +*.pkl diff --git a/developer/README.md b/developer/README.md new file mode 100644 index 00000000..3c45baea --- /dev/null +++ b/developer/README.md @@ -0,0 +1,21 @@ +# Developer setup helper + +To give idea, what is needed to develop rcc. + +## Two task + +### Building the thing + +``` +rcc run -r developer/toolkit.yaml -t build +``` + +### Testing the thing + +``` +rcc run -r developer/toolkit.yaml -t test +``` + +## Dependencies + +Needed dependencies are visible at `developer/setup.yaml` file. diff --git a/developer/builder.py b/developer/builder.py new file mode 100644 index 00000000..802e73a9 --- /dev/null +++ b/developer/builder.py @@ -0,0 +1,4 @@ +from os import chdir, system + +chdir('..') +system('rake build') diff --git a/developer/setup.yaml b/developer/setup.yaml new file mode 100644 index 00000000..c3fcf963 --- /dev/null +++ b/developer/setup.yaml @@ -0,0 +1,7 @@ +channels: +- conda-forge +dependencies: +- python=3.9.15 +- ruby=2.7.2 +- robotframework=3.2.2 +- go=1.20.7 diff --git a/developer/tester.py b/developer/tester.py new file mode 100644 index 00000000..1599b3d4 --- /dev/null +++ b/developer/tester.py @@ -0,0 +1,4 @@ +from os import chdir, system + +chdir('..') +system('rake robot') diff --git a/developer/toolkit.yaml b/developer/toolkit.yaml new file mode 100644 index 00000000..d23f05dc --- /dev/null +++ b/developer/toolkit.yaml @@ -0,0 +1,15 @@ +tasks: + test: + shell: python tester.py + build: + shell: python builder.py + +environmentConfigs: + - setup.yaml + +artifactsDir: tmp + +PATH: +PYTHONPATH: +ignoreFiles: + - .gitignore diff --git a/docs/changelog.md b/docs/changelog.md index 2fe7e41f..f8eaeab1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v18.1.4 (date: 5.8.2024) + +- bugfix: when there is PS1 in holotree variables, it is now filtered out +- developer directory with toolkit.yaml (hidden robot.yaml) + ## v18.1.3 (date: 2.8.2024) - bugfix: tlsCheck was giving nil TLS information without error From 03df00a8bbb9caeeac9135ae5a319b115a3e5532 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Aug 2024 09:00:19 +0300 Subject: [PATCH 513/516] Developer tooling update (v18.1.5) --- Rakefile | 16 ++++++++++++---- common/version.go | 2 +- developer/README.md | 40 ++++++++++++++++++++++++++++++++++------ developer/builder.py | 4 ---- developer/rake.py | 7 +++++++ developer/setup.yaml | 8 +++++--- developer/tester.py | 4 ---- developer/toolkit.yaml | 18 +++++++++++++----- docs/BUILD.md | 4 +++- docs/changelog.md | 4 ++++ 10 files changed, 79 insertions(+), 28 deletions(-) delete mode 100644 developer/builder.py create mode 100644 developer/rake.py delete mode 100644 developer/tester.py diff --git a/Rakefile b/Rakefile index e38329b1..5b03a049 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,11 @@ if Rake::Win32.windows? then PYTHON='python' LS='dir' + WHICH='where' else PYTHON='python3' LS='ls -l' + WHICH='which -a' end desc 'Show latest HEAD with stats' @@ -16,8 +18,9 @@ task :tooling do puts "PATH is #{ENV['PATH']}" puts "GOPATH is #{ENV['GOPATH']}" puts "GOROOT is #{ENV['GOROOT']}" - sh "which -a zip || echo NA" - sh "ls -l $HOME/go/bin" + sh "#{WHICH} git || echo NA" + sh "#{WHICH} sed || echo NA" + sh "#{WHICH} zip || echo NA" end task :noassets do @@ -64,7 +67,12 @@ task :clean do sh 'rm -rf build/' end -task :support do +desc 'Update table of contents on docs/ directory.' +task :toc do + sh "#{PYTHON} scripts/toc.py" +end + +task :support => [:toc] do sh 'mkdir -p tmp build/linux64 build/macos64 build/windows64' end @@ -102,7 +110,7 @@ task :robotsetup do end desc 'Build local, operating system specific rcc' -task :local => [:tooling, :support, :assets] do +task :local => [:tooling, :test] do sh "go build -o build/ ./cmd/..." end diff --git a/common/version.go b/common/version.go index f25a73cd..5bb1b597 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.4` + Version = `v18.1.5` ) diff --git a/developer/README.md b/developer/README.md index 3c45baea..208d35b1 100644 --- a/developer/README.md +++ b/developer/README.md @@ -1,19 +1,47 @@ # Developer setup helper -To give idea, what is needed to develop rcc. +To give idea, what is needed to develop rcc. This is bootstrapping rcc +development with older version of rcc. So, you really need older rcc +installed somewhere available in PATH. -## Two task +This developer toolkit uses both `tasks:` and `devTasks:` to enable tools. +Pay attention for `--dev` flag usage. -### Building the thing +And `WARNING` ... this only works currently on Linux and Mac. Windows is +missing some tools (sed and zip at least) that are needed in development cycle. + +## One task to test the thing with robot + +``` +rcc run -r developer/toolkit.yaml -t robot +``` + +Then see `tmp/output/log.html` for possible failure details. + +## Some developer tasks + +### Building the thing for local OS + +``` +rcc run -r developer/toolkit.yaml --dev -t local +``` + +### Building the thing (all OSes) + +``` +rcc run -r developer/toolkit.yaml --dev -t build +``` + +### Update documentation TOC ``` -rcc run -r developer/toolkit.yaml -t build +rcc run -r developer/toolkit.yaml --dev -t toc ``` -### Testing the thing +### Show tools ``` -rcc run -r developer/toolkit.yaml -t test +rcc run -r developer/toolkit.yaml --dev -t tools ``` ## Dependencies diff --git a/developer/builder.py b/developer/builder.py deleted file mode 100644 index 802e73a9..00000000 --- a/developer/builder.py +++ /dev/null @@ -1,4 +0,0 @@ -from os import chdir, system - -chdir('..') -system('rake build') diff --git a/developer/rake.py b/developer/rake.py new file mode 100644 index 00000000..1c324f30 --- /dev/null +++ b/developer/rake.py @@ -0,0 +1,7 @@ +import os, sys, subprocess +from os import chdir +from subprocess import run + +task = '-T' if len(sys.argv) < 2 else sys.argv[1] +os.chdir('..') +exit(subprocess.run(('rake', task)).returncode) diff --git a/developer/setup.yaml b/developer/setup.yaml index c3fcf963..720f1fec 100644 --- a/developer/setup.yaml +++ b/developer/setup.yaml @@ -1,7 +1,9 @@ channels: - conda-forge dependencies: -- python=3.9.15 -- ruby=2.7.2 -- robotframework=3.2.2 +- python=3.12.4 +- ruby=3.3.3 +- robotframework=7.0.1 - go=1.20.7 +- git=2.46.0 +- sed=4.7 diff --git a/developer/tester.py b/developer/tester.py deleted file mode 100644 index 1599b3d4..00000000 --- a/developer/tester.py +++ /dev/null @@ -1,4 +0,0 @@ -from os import chdir, system - -chdir('..') -system('rake robot') diff --git a/developer/toolkit.yaml b/developer/toolkit.yaml index d23f05dc..fe88036c 100644 --- a/developer/toolkit.yaml +++ b/developer/toolkit.yaml @@ -1,15 +1,23 @@ tasks: - test: - shell: python tester.py + robot: + shell: python rake.py robot + +devTasks: build: - shell: python builder.py + shell: python rake.py build + local: + shell: python rake.py local + tools: + shell: python rake.py tooling + toc: + shell: python rake.py toc environmentConfigs: - - setup.yaml +- setup.yaml artifactsDir: tmp PATH: PYTHONPATH: ignoreFiles: - - .gitignore +- .gitignore diff --git a/docs/BUILD.md b/docs/BUILD.md index 9960660a..1311e3e0 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -9,6 +9,8 @@ Required tools are: - robot for testing the thing - zip to build template zipfiles +See also: developer/README.md and developer/setup.yaml + Internal requirements: - can be seen from go.mod and go.sum files in toplevel directory @@ -25,4 +27,4 @@ Internal requirements: To get started with CLI, start from "cmd" directory, which contains commands executed from CLI, each in separate file (plus additional support files). From there, use your editors code navigation to get to actual underlying -functions. \ No newline at end of file +functions. diff --git a/docs/changelog.md b/docs/changelog.md index f8eaeab1..41277572 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v18.1.5 (date: 7.8.2024) + +- developer directory tooling update + ## v18.1.4 (date: 5.8.2024) - bugfix: when there is PS1 in holotree variables, it is now filtered out From cc09a71890c93ba303a7d05525ad1fe1d55568f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huras?= Date: Wed, 21 Aug 2024 12:18:16 +0200 Subject: [PATCH 514/516] Unit tests fix for MacOS and Windows (v18.1.6) (#65) --- .gitignore | 1 + Rakefile | 2 +- common/version.go | 2 +- developer/README.md | 8 ++++++ developer/setup.yaml | 2 +- developer/toolkit.yaml | 2 ++ docs/changelog.md | 4 +++ htfs/fs_test.go | 23 +++++++++++++++--- htfs/testdata/.gitignore | 3 +++ htfs/testdata/simple_darwin.zip | Bin 0 -> 173430 bytes .../testdata/{simple.zip => simple_linux.zip} | Bin htfs/testdata/simple_windows.zip | Bin 0 -> 173520 bytes 12 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 htfs/testdata/.gitignore create mode 100644 htfs/testdata/simple_darwin.zip rename htfs/testdata/{simple.zip => simple_linux.zip} (100%) create mode 100644 htfs/testdata/simple_windows.zip diff --git a/.gitignore b/.gitignore index 27255eaf..93c93360 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ rcccache.yaml tags .DS_Store .use +.idea/ diff --git a/Rakefile b/Rakefile index 5b03a049..28ea8da5 100644 --- a/Rakefile +++ b/Rakefile @@ -78,6 +78,7 @@ end desc 'Run tests.' task :test => [:support, :assets] do + ENV['GOARCH'] = 'amd64' sh 'go test -cover -coverprofile=tmp/cover.out ./...' sh 'go tool cover -func=tmp/cover.out' end @@ -133,4 +134,3 @@ task :version_txt => :support do end task :default => :build - diff --git a/common/version.go b/common/version.go index 5bb1b597..56f6167a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.5` + Version = `v18.1.6` ) diff --git a/developer/README.md b/developer/README.md index 208d35b1..4aea26c2 100644 --- a/developer/README.md +++ b/developer/README.md @@ -20,6 +20,14 @@ Then see `tmp/output/log.html` for possible failure details. ## Some developer tasks +### Unit tests +``` +rcc run -r developer/toolkit.yaml --dev -t unitTests +``` + +You can also run tests running `rake` directly from your CLI, or run `go test` - when running unit tests +outside of `rake` however, make sure `GOARCH` env variable is set to `amd64`, as some tests may rely on it. + ### Building the thing for local OS ``` diff --git a/developer/setup.yaml b/developer/setup.yaml index 720f1fec..75d74c29 100644 --- a/developer/setup.yaml +++ b/developer/setup.yaml @@ -3,7 +3,7 @@ channels: dependencies: - python=3.12.4 - ruby=3.3.3 -- robotframework=7.0.1 +- robotframework=6.1.1 - go=1.20.7 - git=2.46.0 - sed=4.7 diff --git a/developer/toolkit.yaml b/developer/toolkit.yaml index fe88036c..1d2a1fe8 100644 --- a/developer/toolkit.yaml +++ b/developer/toolkit.yaml @@ -3,6 +3,8 @@ tasks: shell: python rake.py robot devTasks: + unitTests: + shell: python rake.py test build: shell: python rake.py build local: diff --git a/docs/changelog.md b/docs/changelog.md index 41277572..d0612d01 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v18.1.6 (date: 21.8.2024) + +- unit tests suite now works properly on MacOS and Windows + ## v18.1.5 (date: 7.8.2024) - developer directory tooling update diff --git a/htfs/fs_test.go b/htfs/fs_test.go index 8df5f404..1ac64196 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -1,11 +1,13 @@ package htfs_test import ( + "fmt" + "github.com/robocorp/rcc/common" "os" "path/filepath" + "strings" "testing" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/hamlet" "github.com/robocorp/rcc/htfs" ) @@ -35,7 +37,6 @@ func TestHTFSspecification(t *testing.T) { must.Nil(err) must.True(len(before) < 300) wont.Equal(fs.Path, reloaded.Path) - must.Nil(reloaded.LoadFrom(filename)) after, err := reloaded.AsJson() must.Nil(err) @@ -43,15 +44,29 @@ func TestHTFSspecification(t *testing.T) { must.Equal(fs.Path, reloaded.Path) } +// This test case depends on runtime.GOARCH being "amd64" - this is enforced +// when running unit tests with rake, but if the test suite is run otherwise, +// for example directly from the IDE, GOARCH env variable needs to be set in order +// for this test to pass. func TestZipLibrary(t *testing.T) { must, wont := hamlet.Specifications(t) - must.Equal("linux_amd64", common.Platform()) + platform := common.Platform() + var zipFileName string + + switch { + case strings.Contains(platform, "linux"): + zipFileName = "simple_linux.zip" + case strings.Contains(platform, "darwin"): + zipFileName = "simple_darwin.zip" + case strings.Contains(platform, "windows"): + zipFileName = "simple_windows.zip" + } _, blueprint, err := htfs.ComposeFinalBlueprint([]string{"testdata/simple.yaml"}, "") must.Nil(err) wont.Nil(blueprint) - sut, err := htfs.ZipLibrary("testdata/simple.zip") + sut, err := htfs.ZipLibrary(fmt.Sprintf("testdata/%s", zipFileName)) must.Nil(err) wont.Nil(sut) must.True(sut.HasBlueprint(blueprint)) diff --git a/htfs/testdata/.gitignore b/htfs/testdata/.gitignore new file mode 100644 index 00000000..fe22226a --- /dev/null +++ b/htfs/testdata/.gitignore @@ -0,0 +1,3 @@ +!simple_linux.zip +!simple_darwin.zip +!simple_windows.zip diff --git a/htfs/testdata/simple_darwin.zip b/htfs/testdata/simple_darwin.zip new file mode 100644 index 0000000000000000000000000000000000000000..2c2f5613abb1c7fbf40e08e872fa08eff359c335 GIT binary patch literal 173430 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

Cu{!gB||M#T% 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 E3kC$X0{{R3 literal 0 HcmV?d00001 diff --git a/htfs/testdata/simple.zip b/htfs/testdata/simple_linux.zip similarity index 100% rename from htfs/testdata/simple.zip rename to htfs/testdata/simple_linux.zip diff --git a/htfs/testdata/simple_windows.zip b/htfs/testdata/simple_windows.zip new file mode 100644 index 0000000000000000000000000000000000000000..c28c63f5f4c9a85b39efdb828e628ddff0f4c568 GIT binary patch literal 173520 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

Cu{!gB||M#T%hQMwh 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 literal 0 HcmV?d00001 From 9cf5ad016cf52ac97318eb893ac184fc276ced11 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 17 Oct 2024 15:22:30 -0300 Subject: [PATCH 515/516] Use Windows runner in GitHub Actions, use python/invoke instead of ruby/rake. --- .gitattributes | 15 ++ .github/workflows/rcc.yaml | 135 ++++++++-------- .vscode/settings.json | 36 +++++ Rakefile | 136 ---------------- common/version.go | 2 +- developer/README.md | 8 +- developer/call_invoke.py | 9 ++ developer/rake.py | 7 - developer/setup.yaml | 17 +- developer/toolkit.yaml | 16 +- docs/BUILD.md | 11 +- docs/changelog.md | 14 +- htfs/fs_test.go | 5 +- pathlib/sha256_test.go | 7 + robot_requirements.txt | 2 +- robot_tests/exitcodes.robot | 71 +++++---- robot_tests/export_holozip.robot | 126 +++++++-------- robot_tests/supporting.py | 67 +++++++- scripts/deadcode.py | 29 ++-- scripts/toc.py | 74 +++++---- tasks.py | 256 +++++++++++++++++++++++++++++++ 21 files changed, 657 insertions(+), 386 deletions(-) create mode 100644 .gitattributes create mode 100644 .vscode/settings.json delete mode 100644 Rakefile create mode 100644 developer/call_invoke.py delete mode 100644 developer/rake.py create mode 100644 tasks.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e9b7d562 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Set the default behavior, in case people don't have `core.autocrlf` set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native OS line endings on checkout, but committing those with LF in the repo. + +# Required for robot tests to run properly in Windows. +**/testdata/*.txt text eol=lf +**/testdata/*.yaml text eol=lf + +*.py text +*.js text +*.md text +*.robot text +*.go text diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index 0ac5925a..01ec0a33 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -1,69 +1,76 @@ name: Rcc on: - workflow_dispatch: - # enables manual triggering - push: - branches: - - master - - maintenance - - series10 + workflow_dispatch: + # enables manual triggering + push: + branches: + - master + - maintenance + - series10 + pull_request: + branches: + - master jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v5 - with: - go-version: '1.20.x' - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - - uses: actions/checkout@v4 - - name: What - run: rake what - - name: Building - run: rake clean build + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.20.x" + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - uses: actions/checkout@v4 + - name: Install invoke + run: python -m pip install invoke + - name: What + run: inv what + - name: Building + run: inv build - robot: - name: Robot - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: ['ubuntu'] - steps: - - uses: actions/setup-go@v5 - with: - go-version: '1.20.x' - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - - uses: actions/setup-python@v5 - with: - python-version: '3.9' - - uses: actions/checkout@v4 - - name: Setup - run: rake robotsetup - - name: What - run: rake what - - name: Testing - run: rake clean robot - - uses: actions/upload-artifact@v1 - if: success() || failure() - with: - name: ${{ matrix.os }}-test-reports - path: ./tmp/output/ - trigger: - name: Trigger - runs-on: ubuntu-latest - needs: - - build - - robot - steps: - - name: Pipeline - run: | - curl -X POST https://api.github.com/repos/robocorp/rcc-pipeline/dispatches \ - -H 'Accept: application/vnd.github.v3+json' \ - -u ${{ secrets.TRIGGER_TOKEN }} \ - --data '{"event_type": "pipes"}' + robot: + name: Robot + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: ["ubuntu", "windows"] + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.20.x" + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - uses: actions/checkout@v4 + - name: Install invoke + run: python -m pip install invoke + - name: Setup + run: inv robotsetup + - name: What + run: inv what + - name: Testing + run: inv robot + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: ${{ matrix.os }}-test-reports + path: ./tmp/output/ + + trigger: + name: Trigger + runs-on: ubuntu-latest + needs: + - build + - robot + if: success() && github.ref == 'refs/heads/master' + steps: + - name: Pipeline + run: | + curl -X POST https://api.github.com/repos/robocorp/rcc-pipeline/dispatches \ + -H 'Accept: application/vnd.github.v3+json' \ + -u ${{ secrets.TRIGGER_TOKEN }} \ + --data '{"event_type": "pipes"}' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..edf0efca --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,36 @@ +{ + "python.pydev.pythonPath": [ + ".", + ], + "python.pydev.preferredImportLocation": "topOfMethod", + + "python.pydev.formatter": "ruff", + "python.pydev.lint.ruff.use": true, + "python.pydev.lint.ruff.showOutput": true, + "python.pydev.lint.mypy.use": true, + "python.pydev.lint.mypy.showOutput": true, + "python.pydev.lint.mypy.args": "--follow-imports=silent --show-column-numbers --namespace-packages --explicit-package-bases", + "python.pydev.docstring.style": "google", + "editor.formatOnType": true, + "editor.formatOnSave": true, + "python.pydev.sortImportsOnFormat": "isort", + + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 28ea8da5..00000000 --- a/Rakefile +++ /dev/null @@ -1,136 +0,0 @@ -if Rake::Win32.windows? then - PYTHON='python' - LS='dir' - WHICH='where' -else - PYTHON='python3' - LS='ls -l' - WHICH='which -a' -end - -desc 'Show latest HEAD with stats' -task :what do - sh 'go version' - sh 'git --no-pager log -2 --stat HEAD' -end - -task :tooling do - puts "PATH is #{ENV['PATH']}" - puts "GOPATH is #{ENV['GOPATH']}" - puts "GOROOT is #{ENV['GOROOT']}" - sh "#{WHICH} git || echo NA" - sh "#{WHICH} sed || echo NA" - sh "#{WHICH} zip || echo NA" -end - -task :noassets do - rm_f FileList['blobs/assets/micromamba.*'] - rm_f FileList['blobs/assets/*.zip'] - rm_f FileList['blobs/assets/*.yaml'] - rm_f FileList['blobs/assets/*.py'] - rm_f FileList['blobs/assets/man/*.txt'] - rm_f FileList['blobs/docs/*.md'] -end - -def download_link(version, platform, filename) - "https://downloads.robocorp.com/micromamba/#{version}/#{platform}/#{filename}" -end - -task :micromamba do - version = File.read('assets/micromamba_version.txt').strip() - puts "Using micromamba version #{version}" - url = download_link(version, "macos64", "micromamba") - sh "curl -o blobs/assets/micromamba.darwin_amd64 #{url}" - url = download_link(version, "windows64", "micromamba.exe") - sh "curl -o blobs/assets/micromamba.windows_amd64 #{url}" - url = download_link(version, "linux64", "micromamba") - sh "curl -o blobs/assets/micromamba.linux_amd64 #{url}" - sh "gzip -f -9 blobs/assets/micromamba.*" -end - -task :assets => [:noassets, :micromamba] do - FileList['templates/*/'].each do |directory| - basename = File.basename(directory) - assetname = File.absolute_path(File.join("blobs", "assets", "#{basename}.zip")) - rm_rf assetname - puts "Directory #{directory} => #{assetname}" - sh "cd #{directory} && zip -ryqD9 #{assetname} ." - end - cp FileList['assets/*.txt'], 'blobs/assets/' - cp FileList['assets/*.yaml'], 'blobs/assets/' - cp FileList['assets/*.py'], 'blobs/assets/' - cp FileList['assets/man/*.txt'], 'blobs/assets/man/' - cp FileList['docs/*.md'], 'blobs/docs/' -end - -task :clean do - sh 'rm -rf build/' -end - -desc 'Update table of contents on docs/ directory.' -task :toc do - sh "#{PYTHON} scripts/toc.py" -end - -task :support => [:toc] do - sh 'mkdir -p tmp build/linux64 build/macos64 build/windows64' -end - -desc 'Run tests.' -task :test => [:support, :assets] do - ENV['GOARCH'] = 'amd64' - sh 'go test -cover -coverprofile=tmp/cover.out ./...' - sh 'go tool cover -func=tmp/cover.out' -end - -task :linux64 => [:what, :test] do - ENV['GOOS'] = 'linux' - ENV['GOARCH'] = 'amd64' - sh "go build -ldflags '-s' -o build/linux64/ ./cmd/..." - sh "sha256sum build/linux64/* || true" -end - -task :macos64 => [:support] do - ENV['GOOS'] = 'darwin' - ENV['GOARCH'] = 'amd64' - sh "go build -ldflags '-s' -o build/macos64/ ./cmd/..." - sh "sha256sum build/macos64/* || true" -end - -task :windows64 => [:support] do - ENV['GOOS'] = 'windows' - ENV['GOARCH'] = 'amd64' - sh "go build -ldflags '-s' -o build/windows64/ ./cmd/..." - sh "sha256sum build/windows64/* || true" -end - -desc 'Setup build environment' -task :robotsetup do - sh "#{PYTHON} -m pip install --upgrade -r robot_requirements.txt" - sh "#{PYTHON} -m pip freeze" -end - -desc 'Build local, operating system specific rcc' -task :local => [:tooling, :test] do - sh "go build -o build/ ./cmd/..." -end - -desc 'Run robot tests on local application' -task :robot => :local do - sh "robot -L DEBUG -d tmp/output robot_tests" -end - -desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do - sh 'ls -l $(find build -type f)' -end - -def version - `sed -n -e '/Version/{s/^.*\`v//;s/\`$//p}' common/version.go`.strip -end - -task :version_txt => :support do - File.write('build/version.txt', "v#{version}") -end - -task :default => :build diff --git a/common/version.go b/common/version.go index 56f6167a..41303060 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.1.6` + Version = `v18.1.7` ) diff --git a/developer/README.md b/developer/README.md index 4aea26c2..ca8a4cd1 100644 --- a/developer/README.md +++ b/developer/README.md @@ -7,9 +7,6 @@ installed somewhere available in PATH. This developer toolkit uses both `tasks:` and `devTasks:` to enable tools. Pay attention for `--dev` flag usage. -And `WARNING` ... this only works currently on Linux and Mac. Windows is -missing some tools (sed and zip at least) that are needed in development cycle. - ## One task to test the thing with robot ``` @@ -21,12 +18,13 @@ Then see `tmp/output/log.html` for possible failure details. ## Some developer tasks ### Unit tests + ``` rcc run -r developer/toolkit.yaml --dev -t unitTests ``` -You can also run tests running `rake` directly from your CLI, or run `go test` - when running unit tests -outside of `rake` however, make sure `GOARCH` env variable is set to `amd64`, as some tests may rely on it. +You can also run tests running `invoke` directly from your CLI, or run `go test` - when running unit tests +outside of `invoke` however, make sure `GOARCH` env variable is set to `amd64`, as some tests may rely on it. ### Building the thing for local OS diff --git a/developer/call_invoke.py b/developer/call_invoke.py new file mode 100644 index 00000000..c604ba52 --- /dev/null +++ b/developer/call_invoke.py @@ -0,0 +1,9 @@ +import os +import subprocess +import sys + +use_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +assert len(sys.argv) >= 2, "No task provided when calling `call_invoke.py`" +task = sys.argv[1] +exit(subprocess.run(("invoke", task), cwd=use_dir).returncode) diff --git a/developer/rake.py b/developer/rake.py deleted file mode 100644 index 1c324f30..00000000 --- a/developer/rake.py +++ /dev/null @@ -1,7 +0,0 @@ -import os, sys, subprocess -from os import chdir -from subprocess import run - -task = '-T' if len(sys.argv) < 2 else sys.argv[1] -os.chdir('..') -exit(subprocess.run(('rake', task)).returncode) diff --git a/developer/setup.yaml b/developer/setup.yaml index 75d74c29..b955a26b 100644 --- a/developer/setup.yaml +++ b/developer/setup.yaml @@ -1,9 +1,12 @@ channels: -- conda-forge + - conda-forge dependencies: -- python=3.12.4 -- ruby=3.3.3 -- robotframework=6.1.1 -- go=1.20.7 -- git=2.46.0 -- sed=4.7 + # Note: needs to match the version in the GitHub Actions workflow + # (both rcc and rcc-pipeline) + - python=3.10.15 + - invoke=2.2.0 + # Note: needs to match the version in robot_requirements.txt + # Also in rcc-pipeline + - robotframework=6.1.1 + - go=1.20.7 + - git=2.46.0 diff --git a/developer/toolkit.yaml b/developer/toolkit.yaml index 1d2a1fe8..bd9fcc1b 100644 --- a/developer/toolkit.yaml +++ b/developer/toolkit.yaml @@ -1,25 +1,25 @@ tasks: robot: - shell: python rake.py robot + shell: python call_invoke.py robot devTasks: unitTests: - shell: python rake.py test + shell: python call_invoke.py test build: - shell: python rake.py build + shell: python call_invoke.py build local: - shell: python rake.py local + shell: python call_invoke.py local tools: - shell: python rake.py tooling + shell: python call_invoke.py tooling toc: - shell: python rake.py toc + shell: python call_invoke.py toc environmentConfigs: -- setup.yaml + - setup.yaml artifactsDir: tmp PATH: PYTHONPATH: ignoreFiles: -- .gitignore + - .gitignore diff --git a/docs/BUILD.md b/docs/BUILD.md index 1311e3e0..08e80147 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -5,9 +5,8 @@ Required tools are: - golang for implementing the thing -- rake for automating building the thing +- invoke for automating building the thing - robot for testing the thing -- zip to build template zipfiles See also: developer/README.md and developer/setup.yaml @@ -17,10 +16,10 @@ Internal requirements: ## Commands -- to see available tasks, use `rake -T` -- to build everything, use `rake build` command -- to run robot tests, use `rake robot` command -- note, that most of rake commands are build to be used in Github Actions +- to see available tasks, use `inv -l` +- to build everything, use `inv build` command +- to run robot tests, use `inv robot` command +- note, that most of invoke commands are built to be used in Github Actions ## Where to start reading code? diff --git a/docs/changelog.md b/docs/changelog.md index d0612d01..9daf4480 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v18.1.7 (date: 17.10.2024) + +- adding support for windows development of rcc +- using python/invoke instead of ruby/rake for building rcc +- code formatting python code with ruff +- also using windows runner for tests in github actions + ## v18.1.6 (date: 21.8.2024) - unit tests suite now works properly on MacOS and Windows @@ -1215,7 +1222,7 @@ - fix: using wrong file for age calculation on holotree catalogs - fix: holotree check failed to recover on corrupted files; now failure - leads to removal of broken file + leads to removal of broken file - fix: empty hololib directories are now removed on holotree check ## v11.22.0 (date: 31.8.2022) @@ -1705,7 +1712,7 @@ visible changes in rcc commands used. Here is a summary of those changes. holotree blueprint hash. - Old `rcc env plan` was renamed to `rcc holotree plan` and changed to show plan from given holotree space. -- Old `rcc env cleanup` was renamed to `rcc configuration cleanup` and +- Old `rcc env cleanup` was renamed to `rcc configuration cleanup` and changed to work in a way that only holotree things are valid from now on. This means that if you are using `rcc conf cleanup`, check help for changed flags also. @@ -1866,7 +1873,7 @@ visible changes in rcc commands used. Here is a summary of those changes. - when environment creation is serialized, after short delay, rcc reports that it is waiting to be able to contiue -- added __MACOSX as ignored files/directories +- added \_\_MACOSX as ignored files/directories ## v10.6.0 (date: 16.8.2021) @@ -2521,6 +2528,7 @@ visible changes in rcc commands used. Here is a summary of those changes. an error) ## v8.0.12 (date: 18.1.2021) + - Templates conda -channel ordering reverted pending conda-forge chagnes. ## v8.0.10 (date: 18.1.2021) diff --git a/htfs/fs_test.go b/htfs/fs_test.go index 1ac64196..418d47b1 100644 --- a/htfs/fs_test.go +++ b/htfs/fs_test.go @@ -2,12 +2,13 @@ package htfs_test import ( "fmt" - "github.com/robocorp/rcc/common" "os" "path/filepath" "strings" "testing" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/hamlet" "github.com/robocorp/rcc/htfs" ) @@ -45,7 +46,7 @@ func TestHTFSspecification(t *testing.T) { } // This test case depends on runtime.GOARCH being "amd64" - this is enforced -// when running unit tests with rake, but if the test suite is run otherwise, +// when running unit tests with invoke, but if the test suite is run otherwise, // for example directly from the IDE, GOARCH env variable needs to be set in order // for this test to pass. func TestZipLibrary(t *testing.T) { diff --git a/pathlib/sha256_test.go b/pathlib/sha256_test.go index 89df74a8..98c63542 100644 --- a/pathlib/sha256_test.go +++ b/pathlib/sha256_test.go @@ -1,6 +1,8 @@ package pathlib_test import ( + "bytes" + "os" "testing" "github.com/robocorp/rcc/hamlet" @@ -18,6 +20,11 @@ func TestCalculateSha256OfFiles(t *testing.T) { must.Nil(err) must.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digest) + contents, err := os.ReadFile("testdata/hello.txt") + must.Nil(err) + // check that the file has no CR characters + must.Equal(-1, bytes.Index(contents, []byte("\r"))) + digest, err = pathlib.Sha256("testdata/hello.txt") must.Nil(err) must.Equal("d9014c4624844aa5bac314773d6b689ad467fa4e1d1a50a1b8a99d5a95f72ff5", digest) diff --git a/robot_requirements.txt b/robot_requirements.txt index 1f683238..2982349f 100644 --- a/robot_requirements.txt +++ b/robot_requirements.txt @@ -1 +1 @@ -robotframework==3.1.2 +robotframework==6.1.1 diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index 79e8cb0a..66e4f82f 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -1,47 +1,46 @@ *** Settings *** -Library OperatingSystem -Test template Verify exitcodes +Library OperatingSystem +Library supporting.py -*** Test cases *** EXITCODE COMMAND +Test Template Verify exitcodes -General failure of rcc command 1 build/rcc crapiti -h --controller citests - -General output for rcc command 0 build/rcc --controller citests - -Help for rcc command 0 build/rcc -h +*** Test Cases *** EXITCODE COMMAND +General failure of rcc command 1 build/rcc crapiti -h --controller citests +General output for rcc command 0 build/rcc --controller citests +Help for rcc command 0 build/rcc -h Help for rcc assistant subcommand 0 build/rcc assistant -h --controller citests -Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citests +Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citests Help for rcc community subcommand 0 build/rcc community -h --controller citests Help for rcc configure subcommand 0 build/rcc configure -h --controller citests -Help for rcc create subcommand 0 build/rcc create -h --controller citests -Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests -Help for rcc holotree subcommand 0 build/rcc holotree -h --controller citests -Help for rcc help subcommand 0 build/rcc help -h --controller citests -Help for rcc interactive subcommand 0 build/rcc interactive -h --controller citests -Help for rcc internal subcommand 0 build/rcc internal -h --controller citests -Help for rcc man subcommand 0 build/rcc man -h --controller citests -Help for rcc pull subcommand 0 build/rcc pull -h --controller citests -Help for rcc robot subcommand 0 build/rcc robot -h --controller citests -Help for rcc run subcommand 0 build/rcc run -h --controller citests -Help for rcc task subcommand 0 build/rcc task -h --controller citests -Help for rcc tutorial subcommand 0 build/rcc tutorial -h --controller citests -Help for rcc version subcommand 0 build/rcc version -h --controller citests +Help for rcc create subcommand 0 build/rcc create -h --controller citests +Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests +Help for rcc holotree subcommand 0 build/rcc holotree -h --controller citests +Help for rcc help subcommand 0 build/rcc help -h --controller citests +Help for rcc interactive subcommand 0 build/rcc interactive -h --controller citests +Help for rcc internal subcommand 0 build/rcc internal -h --controller citests +Help for rcc man subcommand 0 build/rcc man -h --controller citests +Help for rcc pull subcommand 0 build/rcc pull -h --controller citests +Help for rcc robot subcommand 0 build/rcc robot -h --controller citests +Help for rcc run subcommand 0 build/rcc run -h --controller citests +Help for rcc task subcommand 0 build/rcc task -h --controller citests +Help for rcc tutorial subcommand 0 build/rcc tutorial -h --controller citests +Help for rcc version subcommand 0 build/rcc version -h --controller citests +Run rcc config settings 0 build/rcc config settings --controller citests +Run rcc docs changelog 0 build/rcc docs changelog --controller citests +Run rcc docs license 0 build/rcc docs license --controller citests +Run rcc docs recipes 0 build/rcc docs recipes --controller citests +Run rcc docs tutorial 0 build/rcc docs tutorial --controller citests +Run rcc holotree list 0 build/rcc holotree list --controller citests +Run rcc tutorial 0 build/rcc tutorial --controller citests +Run rcc version 0 build/rcc version --controller citests +Run rcc --version 0 build/rcc --version --controller citests -Run rcc config settings 0 build/rcc config settings --controller citests -Run rcc docs changelog 0 build/rcc docs changelog --controller citests -Run rcc docs license 0 build/rcc docs license --controller citests -Run rcc docs recipes 0 build/rcc docs recipes --controller citests -Run rcc docs tutorial 0 build/rcc docs tutorial --controller citests -Run rcc holotree list 0 build/rcc holotree list --controller citests -Run rcc tutorial 0 build/rcc tutorial --controller citests -Run rcc version 0 build/rcc version --controller citests -Run rcc --version 0 build/rcc --version --controller citests *** Keywords *** - Verify exitcodes - [Arguments] ${exitcode} ${command} - ${code} ${output}= Run and return rc and output ${command} - Log
${output}
html=yes - Should be equal as strings ${exitcode} ${code} + [Arguments] ${exitcode} ${command} + ${code} ${output} ${error}= Run and return code output error ${command} + Log STDOUT
${output}
html=yes + Log STDERR
${error}
html=yes + Should be equal as strings ${exitcode} ${code} diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 322e45dc..9e30b6e9 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -1,9 +1,9 @@ *** Settings *** -Library OperatingSystem -Library supporting.py -Resource resources.robot -Suite Setup Export setup -Suite Teardown Export teardown +Library OperatingSystem +Library supporting.py +Resource resources.robot +Suite Setup Export setup +Suite Teardown Export teardown *** Keywords *** Export setup @@ -22,80 +22,80 @@ Export teardown *** Test cases *** Goal: Create extended robot into tmp/standalone folder using force. - Step build/rcc robot init --controller citests -t extended -d tmp/standalone -f - Use STDERR - Must Have OK. + Step build/rcc robot init --controller citests -t extended -d tmp/standalone -f + Use STDERR + Must Have OK. - ${output}= Capture Flat Output build/rcc ht hash --silent tmp/standalone/conda.yaml - Set Suite Variable ${fingerprint} ${output} + ${output}= Capture Flat Output build/rcc ht hash --silent tmp/standalone/conda.yaml + Set Suite Variable ${fingerprint} ${output} Goal: Create environment for standalone robot - Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml - Must Have RCC_ENVIRONMENT_HASH= - Must Have RCC_INSTALLATION_ID= - Must Have 4e67cd8_fcb4b859 - Use STDERR - Must Have Progress: 01/15 - Must Have Progress: 15/15 + Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have 4e67cd8_fcb4b859 + Use STDERR + Must Have Progress: 01/15 + Must Have Progress: 15/15 Goal: Must have author space visible - Step build/rcc ht ls - Use STDERR - Must Have 4e67cd8_fcb4b859 - Must Have rcc.citests - Must Have author - Must Have ${fingerprint} - Wont Have guest + Step build/rcc ht ls + Use STDERR + Must Have 4e67cd8_fcb4b859 + Must Have rcc.citests + Must Have author + Must Have ${fingerprint} + Wont Have guest Goal: Show exportable environment list - Step build/rcc ht export - Use STDERR - Must Have Selectable catalogs - Must Have - ${fingerprint} - Must Have OK. + Step build/rcc ht export + Use STDERR + Must Have Selectable catalogs + Must Have - ${fingerprint} + Must Have OK. Goal: Export environment for standalone robot - Step build/rcc ht export -z tmp/standalone/hololib.zip ${fingerprint} - Use STDERR - Wont Have Selectable catalogs - Must Have OK. + Step build/rcc ht export -z tmp/standalone/hololib.zip ${fingerprint} + Use STDERR + Wont Have Selectable catalogs + Must Have OK. Goal: Wrap the robot - Step build/rcc robot wrap -z tmp/full.zip -d tmp/standalone/ - Use STDERR - Must Have OK. + Step build/rcc robot wrap -z tmp/full.zip -d tmp/standalone/ + Use STDERR + Must Have OK. Goal: See contents of that robot - Step unzip -v tmp/full.zip - Must Have robot.yaml - Must Have conda.yaml - Must Have hololib.zip + Step unzip -v tmp/full.zip + Must Have robot.yaml + Must Have conda.yaml + Must Have hololib.zip Goal: Can delete author space - Step build/rcc ht delete 4e67cd8_fcb4b859 - Step build/rcc ht ls - Use STDERR - Wont Have 4e67cd8_fcb4b859 - Wont Have rcc.citests - Wont Have author - Wont Have ${fingerprint} - Wont Have guest + Step build/rcc ht delete 4e67cd8_fcb4b859 + Step build/rcc ht ls + Use STDERR + Wont Have 4e67cd8_fcb4b859 + Wont Have rcc.citests + Wont Have author + Wont Have ${fingerprint} + Wont Have guest Goal: Can run as guest - Fire And Forget build/rcc ht delete 4e67cd8 - Prepare Robocorp Home tmp/guest - Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' - Use STDERR - Must Have point of view, "actual main robot run" was SUCCESS. - Must Have OK. + Fire And Forget build/rcc ht delete 4e67cd8 + Prepare Robocorp Home tmp/guest + Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t "run example task" + Use STDERR + Must Have point of view, "actual main robot run" was SUCCESS. + Must Have OK. Goal: Space created under author for guest - Prepare Robocorp Home tmp/developer - Step build/rcc ht ls - Use STDERR - Wont Have 4e67cd8_fcb4b859 - Wont Have author - Must Have rcc.citests - Must Have ${fingerprint} - Must Have 4e67cd8_aacf1552 - Must Have guest + Prepare Robocorp Home tmp/developer + Step build/rcc ht ls + Use STDERR + Wont Have 4e67cd8_fcb4b859 + Wont Have author + Must Have rcc.citests + Must Have ${fingerprint} + Must Have 4e67cd8_aacf1552 + Must Have guest diff --git a/robot_tests/supporting.py b/robot_tests/supporting.py index bbfdbded..ca15586e 100644 --- a/robot_tests/supporting.py +++ b/robot_tests/supporting.py @@ -1,18 +1,77 @@ import json +import logging import subprocess +import sys + +log = logging.getLogger(__name__) + + +def fix_command(command): + if sys.platform == "win32": + command = command.replace("build/rcc", ".\\build\\rcc.exe", 1) + return command + + +def get_cwd(): + import os + + cwd = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + detail = ( + "(rcc doesn't seem to be built, please run `inv local` before running tests)" + ) + assert "build" in os.listdir(cwd), f"Missing build directory in: {cwd!r} {detail}" + + build_dir = os.path.join(cwd, "build") + if sys.platform == "win32": + assert "rcc.exe" in os.listdir( + build_dir + ), f"Missing rcc.exe in: {build_dir!r} {detail}" + else: + assert "rcc" in os.listdir(build_dir), f"Missing rcc in: {build_dir!r} {detail}" + return cwd + + +def log_command(command: str, cwd: str): + msg = f"Running command: {command!r} cwd: {cwd!r}" + log.info(msg) + def capture_flat_output(command): - task = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + command = fix_command(command) + cwd = get_cwd() + log_command(command, cwd) + + task = subprocess.Popen( + command, + shell=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + cwd=cwd, + ) out, _ = task.communicate() - assert task.returncode == 0, f'Unexpected exit code {task.returncode} from {command!r}' + assert ( + task.returncode == 0 + ), f"Unexpected exit code {task.returncode} from {command!r}" return out.decode().strip() + def run_and_return_code_output_error(command): - task = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + command = fix_command(command) + cwd = get_cwd() + log_command(command, cwd) + + task = subprocess.Popen( + command, + shell=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + cwd=cwd, + ) out, err = task.communicate() return task.returncode, out.decode(), err.decode() + def parse_json(content): parsed = json.loads(content) - assert isinstance(parsed, (list, dict)), f'Expecting list or dict; got {parsed!r}' + assert isinstance(parsed, (list, dict)), f"Expecting list or dict; got {parsed!r}" return parsed diff --git a/scripts/deadcode.py b/scripts/deadcode.py index ca60d1a5..21a22df3 100755 --- a/scripts/deadcode.py +++ b/scripts/deadcode.py @@ -6,29 +6,35 @@ import sys from collections import defaultdict -FUNC_PATTERN = re.compile(r'^\s*func\s+(\w+)') +FUNC_PATTERN = re.compile(r"^\s*func\s+(\w+)") + def read_file(filename): with open(filename) as source: for index, line in enumerate(source): - yield index+1, line + yield index + 1, line + def find_files(where, pattern): - return tuple(sorted(x.relative_to(where) for x in pathlib.Path(where).rglob(pattern))) + return tuple( + sorted(x.relative_to(where) for x in pathlib.Path(where).rglob(pattern)) + ) + def find_pattern(pattern, fileset): for filename in fileset: for number, line in read_file(filename): for item in pattern.finditer(line): - yield f'{filename}:{number}', item.group(1) + yield f"{filename}:{number}", item.group(1) + def process(limit): functions = defaultdict(set) - files = find_files(os.getcwd(), '*.go') + files = find_files(os.getcwd(), "*.go") for filename, function in find_pattern(FUNC_PATTERN, files): functions[function].add(filename) - keys = '|'.join(sorted(functions.keys())) - pattern = re.compile(f'({keys})') + keys = "|".join(sorted(functions.keys())) + pattern = re.compile(f"({keys})") counters = defaultdict(int) linerefs = defaultdict(set) width = 0 @@ -37,14 +43,15 @@ def process(limit): linerefs[value].add(fileref) width = max(width, len(fileref)) for key, value in sorted(counters.items()): - if key.startswith('Test'): + if key.startswith("Test"): continue definitions = len(functions[key]) - 1 if value != limit + definitions: continue for link in sorted(linerefs[key]): - fill = ' ' * (width - len(link)) - print(f'{link}{fill} {key}') + fill = " " * (width - len(link)) + print(f"{link}{fill} {key}") + -if __name__ == '__main__': +if __name__ == "__main__": process(int(sys.argv[1]) if len(sys.argv) > 1 else 1) diff --git a/scripts/toc.py b/scripts/toc.py index 55df6a59..0527a78f 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -4,40 +4,42 @@ import re from os.path import basename -DELETE_PATTERN = re.compile(r'[/:]+') -NONCHAR_PATTERN = re.compile(r'[^.a-z0-9_-]+') -HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') -CODE_PATTERN = re.compile(r'^\s*[`]{3}') +DELETE_PATTERN = re.compile(r"[/:]+") +NONCHAR_PATTERN = re.compile(r"[^.a-z0-9_-]+") +HEADING_PATTERN = re.compile(r"^\s*(#{1,3})\s+(.*?)\s*$") +CODE_PATTERN = re.compile(r"^\s*[`]{3}") -DOT = '.' -DASH = '-' -NEWLINE = '\n' +DOT = "." +DASH = "-" +NEWLINE = "\n" -IGNORE_LIST = ('changelog.md', 'toc.md', 'BUILD.md', 'README.md') +IGNORE_LIST = ("changelog.md", "toc.md", "BUILD.md", "README.md") PRIORITY_LIST = ( - 'docs/usecases.md', - 'docs/features.md', - 'docs/recipes.md', - 'docs/profile_configuration.md', - 'docs/environment-caching.md', - 'docs/maintenance.md', - 'docs/venv.md', - 'docs/troubleshooting.md', - 'docs/vocabulary.md', - 'docs/history.md', - ) + "docs/usecases.md", + "docs/features.md", + "docs/recipes.md", + "docs/profile_configuration.md", + "docs/environment-caching.md", + "docs/maintenance.md", + "docs/venv.md", + "docs/troubleshooting.md", + "docs/vocabulary.md", + "docs/history.md", +) + def unify(value): - low = DELETE_PATTERN.sub('', str(value).lower()) - return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))).replace('.', '') + low = DELETE_PATTERN.sub("", str(value).lower()) + return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))).replace(".", "") + class Toc: def __init__(self, title, baseurl): self.title = title self.baseurl = baseurl self.levels = [0] - self.toc = [f'# {title}'] + self.toc = [f"# {title}"] def leveling(self, level): levelup = True @@ -52,18 +54,21 @@ def leveling(self, level): def add(self, filename, level, title): self.leveling(level) numbering = DOT.join(map(str, self.levels)) - url = f'{self.baseurl}{filename}' - prefix = '#' * level + url = f"{self.baseurl}{filename}" + prefix = "#" * level ref = unify(title) - self.toc.append(f'#{prefix} {numbering} [{title}]({self.baseurl}{filename}#{ref})') + self.toc.append( + f"#{prefix} {numbering} [{title}]({self.baseurl}{filename}#{ref})" + ) def write(self, filename): - with open(filename, 'w+') as sink: + with open(filename, "w+") as sink: sink.write(NEWLINE.join(self.toc)) + def headings(filename): inside = False - with open(filename) as source: + with open(filename, encoding="utf-8") as source: for line in source: if CODE_PATTERN.match(line): inside = not inside @@ -73,9 +78,13 @@ def headings(filename): level, title = found.groups() yield filename, len(level), title + def process(): - toc = Toc("Table of contents: rcc documentation", "https://github.com/robocorp/rcc/blob/master/") - flatnames = list(map(basename, glob.glob('docs/*.md'))) + toc = Toc( + "Table of contents: rcc documentation", + "https://github.com/robocorp/rcc/blob/master/", + ) + flatnames = list(map(basename, glob.glob("docs/*.md"))) for filename in PRIORITY_LIST: flatname = basename(filename) if flatname in flatnames: @@ -85,9 +94,10 @@ def process(): for flatname in flatnames: if flatname in IGNORE_LIST: continue - for filename, level, title in headings(f'docs/{flatname}'): + for filename, level, title in headings(f"docs/{flatname}"): toc.add(filename, level, title) - toc.write('docs/README.md') + toc.write("docs/README.md") + -if __name__ == '__main__': +if __name__ == "__main__": process() diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..0d41d1c4 --- /dev/null +++ b/tasks.py @@ -0,0 +1,256 @@ +import os +import shutil +import sys + +from invoke import task + +# Determine OS-specific commands +if sys.platform == "win32": + PYTHON = "python" + LS = "dir" + WHICH = "where" +else: + PYTHON = "python3" + LS = "ls -l" + WHICH = "which -a" + + +@task +def what(c): + """Show latest HEAD with stats""" + c.run("go version") + c.run("git --no-pager log -2 --stat HEAD") + + +@task +def tooling(c): + """Display tooling information""" + print(f"PATH is {os.environ['PATH']}") + print(f"GOPATH is {os.environ.get('GOPATH', 'Not set')}") + print(f"GOROOT is {os.environ.get('GOROOT', 'Not set')}") + print("git info:") + c.run(f"{WHICH} git || echo NA") + + +@task +def noassets(c): + """Remove asset files""" + import glob + + patterns = [ + "blobs/assets/micromamba.*", + "blobs/assets/*.zip", + "blobs/assets/*.yaml", + "blobs/assets/*.py", + "blobs/assets/man/*.txt", + "blobs/docs/*.md", + ] + for pattern in patterns: + for file_path in glob.glob(pattern, recursive=True): + try: + os.remove(file_path) + print(f"Removed: {file_path}") + except OSError as e: + print(f"Error removing {file_path}: {e}") + + +def download_link(version, platform, filename): + return f"https://downloads.robocorp.com/micromamba/{version}/{platform}/{filename}" + + +@task +def micromamba(c): + """Download micromamba files""" + with open("assets/micromamba_version.txt", "r", encoding="utf-8") as f: + version = f.read().strip() + print(f"Using micromamba version {version}") + + platforms = { + "macos64": "darwin_amd64", + "windows64": "windows_amd64", + "linux64": "linux_amd64", + } + + for platform, arch in platforms.items(): + filename = "micromamba.exe" if platform == "windows64" else "micromamba" + url = download_link(version, platform, filename) + output = f"blobs/assets/micromamba.{arch}" + if os.path.exists(output + ".gz"): + print(f"Asset {output}.gz already exists, skipping") + continue + print(f"Downloading {url} to {output}") + c.run(f"curl -o {output} {url}") + print(f"Compressing {output}") + c.run(f"gzip -f -9 {output}") + + +@task(pre=[micromamba]) +def assets(c): + """Prepare asset files""" + import glob + from zipfile import ZIP_DEFLATED, ZipFile + + # Process template directories + for directory in glob.glob("templates/*/"): + basename = os.path.basename(os.path.dirname(directory)) + assetname = os.path.abspath(f"blobs/assets/{basename}.zip") + + if os.path.exists(assetname): + print(f"Asset {assetname} already exists, skipping") + continue + + print(f"Directory {directory} => {assetname}") + + with ZipFile(assetname, "w", ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, directory) + zipf.write(file_path, arcname) + + # Copy asset files + asset_patterns = ["assets/*.txt", "assets/*.yaml", "assets/*.py"] + for pattern in asset_patterns: + for file in glob.glob(pattern): + print(f"Copying {file} to blobs/assets/") + shutil.copy(file, "blobs/assets/") + + # Copy man pages + os.makedirs("blobs/assets/man", exist_ok=True) + for file in glob.glob("assets/man/*.txt"): + print(f"Copying {file} to blobs/assets/man/") + shutil.copy(file, "blobs/assets/man/") + + # Copy docs + os.makedirs("blobs/docs", exist_ok=True) + for file in glob.glob("docs/*.md"): + print(f"Copying {file} to blobs/docs/") + shutil.copy(file, "blobs/docs/") + + +@task(pre=[noassets]) +def clean(c): + """Remove build directory""" + shutil.rmtree("build", ignore_errors=True) + print("Removed build directory") + + +@task +def toc(c): + """Update table of contents on docs/ directory""" + c.run(f"{PYTHON} scripts/toc.py") + print("Ran scripts/toc.py") + + +@task(pre=[toc]) +def support(c): + """Create necessary directories""" + for dir in ["tmp", "build/linux64", "build/macos64", "build/windows64"]: + os.makedirs(dir, exist_ok=True) + + +@task(pre=[support, assets]) +def test(c, cover=False): + """Run tests""" + os.environ["GOARCH"] = "amd64" + if cover: + c.run("go test -cover -coverprofile=tmp/cover.out ./...") + c.run("go tool cover -func=tmp/cover.out") + else: + c.run("go test ./...") + + +def version() -> str: + import re + + with open("common/version.go", "r") as file: + content = file.read() + match = re.search(r"Version\s*=\s*`v([^`]+)`", content) + if match: + return match.group(1) + else: + raise ValueError("Version not found in common/version.go") + + +@task +def version_txt(c): + """Create version.txt file""" + support(c) + target = "build/version.txt" + v = version() + with open(target, "w") as f: + f.write(f"v{v}") + print(f"Created {target} with version {v}") + + +@task(pre=[support, version_txt, assets]) +def build(c, platform="all"): + """Build executables""" + from pathlib import Path + + os.environ["CGO_ENABLED"] = "0" + os.environ["GOARCH"] = "amd64" + + build_platforms = ["linux", "darwin", "windows"] + + if platform == "all": + platforms = build_platforms + else: + assert platform in build_platforms, f"Invalid platform: {platform}" + platforms = [platform] + + for goos in platforms: + os.environ["GOOS"] = goos + output = f"build/{goos}64/" + + c.run(f"go build -ldflags -s -o {output} ./cmd/...") + + ext = ".exe" if goos == "windows" else "" + f = f"{output}rcc{ext}" + assert Path(f).exists(), f"File {f} does not exist" + print(f"Built: {f}") + + +@task +def windows64(c): + """Build windows64 executable""" + build(c, platform="windows") + + +@task +def linux64(c): + """Build linux64 executable""" + build(c, platform="linux") + + +@task +def macos64(c): + """Build macos64 executable""" + build(c, platform="darwin") + + +@task +def robotsetup(c): + """Setup build environment""" + if not os.path.exists("robot_requirements.txt"): + raise RuntimeError( + f"robot_requirements.txt not found. Current directory: {os.path.abspath(os.getcwd())}" + ) + c.run(f"{PYTHON} -m pip install --upgrade -r robot_requirements.txt") + c.run(f"{PYTHON} -m pip freeze") + + +@task +def local(c, do_test=True): + """Build local, operating system specific rcc""" + tooling(c) + if do_test: + test(c) + c.run("go build -o build/ ./cmd/...") + + +@task(pre=[robotsetup, assets, local]) +def robot(c): + """Run robot tests on local application""" + print("Running robot tests...") + c.run(f"{PYTHON} -m robot -L DEBUG -d tmp/output robot_tests") From 2cd5d7b790b98f82c7455fd2a5875bf0f658bfb1 Mon Sep 17 00:00:00 2001 From: yiwan330445 <149597955+yiwan330445@users.noreply.github.com> Date: Fri, 27 Dec 2024 02:52:26 +0800 Subject: [PATCH 516/516] Create generator-generic-ossf-slsa3-publish.yml --- .../generator-generic-ossf-slsa3-publish.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/generator-generic-ossf-slsa3-publish.yml diff --git a/.github/workflows/generator-generic-ossf-slsa3-publish.yml b/.github/workflows/generator-generic-ossf-slsa3-publish.yml new file mode 100644 index 00000000..35c829b1 --- /dev/null +++ b/.github/workflows/generator-generic-ossf-slsa3-publish.yml @@ -0,0 +1,66 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow lets you generate SLSA provenance file for your project. +# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements +# The project is an initiative of the OpenSSF (openssf.org) and is developed at +# https://github.com/slsa-framework/slsa-github-generator. +# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier. +# For more information about SLSA and how it improves the supply-chain, visit slsa.dev. + +name: SLSA generic generator +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + outputs: + digests: ${{ steps.hash.outputs.digests }} + + steps: + - uses: actions/checkout@v4 + + # ======================================================== + # + # Step 1: Build your artifacts. + # + # ======================================================== + - name: Build artifacts + run: | + # These are some amazing artifacts. + echo "artifact1" > artifact1 + echo "artifact2" > artifact2 + + # ======================================================== + # + # Step 2: Add a step to generate the provenance subjects + # as shown below. Update the sha256 sum arguments + # to include all binaries that you generate + # provenance for. + # + # ======================================================== + - name: Generate subject for provenance + id: hash + run: | + set -euo pipefail + + # List the artifacts the provenance will refer to. + files=$(ls artifact*) + # Generate the subjects (base64 encoded). + echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}" + + provenance: + needs: [build] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + with: + base64-subjects: "${{ needs.build.outputs.digests }}" + upload-assets: true # Optional: Upload to a new release

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 Date: Tue, 26 Apr 2022 13:46:07 +0300 Subject: [PATCH 253/516] Documentation updates (v11.11.2) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ docs/recipes.md | 19 ++++++++++--------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 2bd4f3d0..56bf846c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.11.1` + Version = `v11.11.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index af590cb4..ee6be173 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.11.2 (date: 26.4.2022) + +- more documentation update for devTasks + ## v11.11.1 (date: 26.4.2022) - bugfix: added v12 indicator in new form of holotree catalogs (to separate diff --git a/docs/recipes.md b/docs/recipes.md index cbf47fd1..cb11d24e 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -475,15 +475,16 @@ compared to normal `tasks:` definitions: created and managed as with normal tasks, but without pre-run scripts applied. -Their primary goal is provide developers way to use same tooling to automate -their development process, like normal `tasks:` provide ways to automate -robot actions. Some examples could be common editor setups and version control -repository updates. - -Currently `--dev` option is only available in `rcc run` and `rcc task run` -commands. And when that `--dev` option is given on command line, only -`devTasks:` are available for running and all normal `tasks:` are missing. -And when that option is missing, then also `devTasks:` are invisible/missing. +The `devTasks:` primary goal is to provide developers a way to use the same +tooling to automate their development process as normal `tasks:` provide ways +to automate robot actions. Some examples could be: common editor setups, +version control repository updates. + +Currently `--dev` option is only available for `rcc run` and `rcc task run` +commands. With the `--dev` option the only available tasks for execution will +be the `devTasks:`. The normal `tasks:` will be skipped/missing. If the `--dev` +option is missing, the `devTasks:` will be skipped/missing, and the normal +`tasks:` will be the ones available for execution. ### What is `condaConfigFile:`? From bc906397ee49ea991df8b9e1b83f23285e07d07c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 28 Apr 2022 13:52:53 +0300 Subject: [PATCH 254/516] Hololib URL imports (v11.12.0) - `rcc holotree import` now supports URL imports --- cloud/client.go | 4 +++- cmd/holotreeImport.go | 36 ++++++++++++++++++++++++++++++++++-- common/version.go | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index 1bdbbc85..35e27b1d 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -211,11 +211,13 @@ func Download(url, filename string) error { common.Debug("Downloading %s <%s> -> %s", url, response.Status, filename) - _, err = io.Copy(many, response.Body) + bytecount, err := io.Copy(many, response.Body) if err != nil { return err } + common.Timeline("downloaded %d bytes to %s", bytecount, filename) + err = out.Sync() if err != nil { return err diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go index 6c9aa0d5..6a1c7e54 100644 --- a/cmd/holotreeImport.go +++ b/cmd/holotreeImport.go @@ -1,23 +1,55 @@ package cmd import ( + "fmt" + "net/url" + "os" + "path/filepath" + + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) +func isUrl(name string) bool { + link, err := url.Parse(name) + if err != nil { + return false + } + return link.IsAbs() && (link.Scheme == "http" || link.Scheme == "https") +} + +func temporaryDownload(at int, link string) (string, error) { + common.Timeline("Download %v", link) + zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("hololib%x%x.zip", common.When, at)) + err := cloud.Download(link, zipfile) + if err != nil { + return "", err + } + return zipfile, nil +} + var holotreeImportCmd = &cobra.Command{ Use: "import hololib.zip+", Short: "Import one or more hololib.zip files into local hololib.", Long: "Import one or more hololib.zip files into local hololib.", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + var err error + if common.DebugFlag { defer common.Stopwatch("Holotree import command lasted").Report() } - for _, filename := range args { - err := operations.Unzip(common.HololibLocation(), filename, true, false) + for at, filename := range args { + if isUrl(filename) { + filename, err = temporaryDownload(at, filename) + pretty.Guard(err == nil, 2, "Could not download %q, reason: %v", filename, err) + defer os.Remove(filename) + } + common.Timeline("Import %v", filename) + err = operations.Unzip(common.HololibLocation(), filename, true, false) pretty.Guard(err == nil, 1, "Could not import %q, reason: %v", filename, err) } pretty.Ok() diff --git a/common/version.go b/common/version.go index 56bf846c..5a490ccd 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.11.2` + Version = `v11.12.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index ee6be173..493635e0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.12.0 (date: 28.4.2022) + +- `rcc holotree import` now supports URL imports + ## v11.11.2 (date: 26.4.2022) - more documentation update for devTasks From 752f5554f135717001dde0046b2b9d1a150b0713 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 29 Apr 2022 14:53:40 +0300 Subject: [PATCH 255/516] BUGFIX: note about devTasks (v11.12.1) - bugfix: duplicate devTask note when error needs to be shown --- common/version.go | 2 +- docs/changelog.md | 4 ++++ robot/robot.go | 12 +++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 5a490ccd..607156cf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.0` + Version = `v11.12.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 493635e0..9bdaaae5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.12.1 (date: 29.4.2022) + +- bugfix: duplicate devTask note when error needs to be shown + ## v11.12.0 (date: 28.4.2022) - `rcc holotree import` now supports URL imports diff --git a/robot/robot.go b/robot/robot.go index 02a46b3a..1ee12946 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -73,9 +73,11 @@ type task struct { robot *robot } -func (it *robot) taskMap() map[string]*task { +func (it *robot) taskMap(note bool) map[string]*task { if common.DeveloperFlag { - pretty.Note("Operating in developer mode. Using 'devTasks:' instead of 'tasks:'.") + if note { + pretty.Note("Operating in developer mode. Using 'devTasks:' instead of 'tasks:'.") + } return it.Devtasks } else { return it.Tasks @@ -297,7 +299,7 @@ func (it *robot) IgnoreFiles() []string { } func (it *robot) AvailableTasks() []string { - tasks := it.taskMap() + tasks := it.taskMap(false) result := make([]string, 0, len(tasks)) for name, _ := range tasks { result = append(result, fmt.Sprintf("%q", name)) @@ -307,7 +309,7 @@ func (it *robot) AvailableTasks() []string { } func (it *robot) DefaultTask() Task { - tasks := it.taskMap() + tasks := it.taskMap(true) if len(tasks) != 1 { return nil } @@ -323,7 +325,7 @@ func (it *robot) TaskByName(name string) Task { if len(name) == 0 { return it.DefaultTask() } - tasks := it.taskMap() + tasks := it.taskMap(true) key := strings.TrimSpace(name) found, ok := tasks[key] if ok { From c4ca56780aca5a0d321c8672cd550bec57191e4b Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 5 May 2022 14:02:02 +0300 Subject: [PATCH 256/516] Assuming some enterprise proxy is set in the old way of using x prefix for custom headers and stripping out anything else (#33) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/authorize.go | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 607156cf..ae50c265 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.1` + Version = `v11.12.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9bdaaae5..e68ae4a5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.12.2 (date: 5.5.2022) + +- legacyfix: Adding `x-` prefix to custom header, due to some enterprise network proxies stripping headers. + ## v11.12.1 (date: 29.4.2022) - bugfix: duplicate devTask note when error needs to be shown diff --git a/operations/authorize.go b/operations/authorize.go index f24e33cc..26f46a70 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -20,7 +20,7 @@ const ( contentType = `content-type` contentLength = `content-length` authorization = `authorization` - nonceHeader = `authorization-timestamp` + nonceHeader = `x-authorization-timestamp` applicationJson = `application/json` applicationOctetStream = `application/octet-stream` WorkspaceApi = `/token-vendor-v1/workspaces/%s/tokenrequest` From ab9e39273e878b9075e7bbcb868f22fc677ee9dd Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 5 May 2022 19:11:45 +0300 Subject: [PATCH 257/516] Reverted header change in v11.12.2 after tests --- common/version.go | 2 +- docs/changelog.md | 6 +++++- operations/authorize.go | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index ae50c265..c553caef 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.2` + Version = `v11.12.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index e68ae4a5..cd3bf700 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v11.12.2 (date: 5.5.2022) +## v11.12.3 (date: 5.5.2022) + +- Reverted the change in v11.12.2 based on further testing. + +## v11.12.2 (date: 5.5.2022) UNSTABLE - legacyfix: Adding `x-` prefix to custom header, due to some enterprise network proxies stripping headers. diff --git a/operations/authorize.go b/operations/authorize.go index 26f46a70..f24e33cc 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -20,7 +20,7 @@ const ( contentType = `content-type` contentLength = `content-length` authorization = `authorization` - nonceHeader = `x-authorization-timestamp` + nonceHeader = `authorization-timestamp` applicationJson = `application/json` applicationOctetStream = `application/octet-stream` WorkspaceApi = `/token-vendor-v1/workspaces/%s/tokenrequest` From cc1f9898cdd73ad6dba621c41d2f10282f5b5e48 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 9 May 2022 12:10:50 +0300 Subject: [PATCH 258/516] BUGFIX: rcc task script problem (v11.12.4) --- common/version.go | 2 +- docs/changelog.md | 7 ++++++- robot/robot.go | 2 +- robot_tests/bug_reports.robot | 8 ++++++++ robot_tests/profiles.robot | 1 - 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index c553caef..4afadf90 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.3` + Version = `v11.12.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index cd3bf700..376a6cc2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # rcc change log -## v11.12.3 (date: 5.5.2022) +## v11.12.4 (date: 9.5.2022) + +- bugfix: rcc task script could not find any task (reason: internal quoting) +- this closes #32 + +## v11.12.3 (date: 5.5.2022) UNSTABLE - Reverted the change in v11.12.2 based on further testing. diff --git a/robot/robot.go b/robot/robot.go index 1ee12946..2dc5055c 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -326,7 +326,7 @@ func (it *robot) TaskByName(name string) Task { return it.DefaultTask() } tasks := it.taskMap(true) - key := strings.TrimSpace(name) + key := strings.Trim(name, "\t\r\n\"' ") found, ok := tasks[key] if ok { return found diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index fa156046..a6af6efa 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -31,6 +31,14 @@ Bug in virtual holotree with gzipped files Use STDERR Must Have Blueprint "ef0163b57ff44cd5" is available: true +Github issue 32 about rcc task script command failing + [Tags] WIP + + Step build/rcc task script --controller citests --robot robot_tests/spellbug/robot.yaml -- pip list + Use STDOUT + Must Have pyspellchecker + Must Have 0.6.2 + *** Keywords *** diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index 697d744b..ea04d083 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -2,7 +2,6 @@ Library OperatingSystem Library supporting.py Resource resources.robot -Default Tags WIP *** Test cases *** From e396b9201aee1da9ad35cddb16cc3b0ca29aa6e0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 9 May 2022 13:38:08 +0300 Subject: [PATCH 259/516] UPGRADE: micromamba upgrade (v11.12.5) --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 4 ++++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 4afadf90..7159e044 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.4` + Version = `v11.12.5` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index cbde0971..0d9e93e8 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.22.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 5bd3d013..9c15aa4a 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.22.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index bd676f70..f1fbcc2e 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.22.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.23.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 7f200500..39b041b0 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -194,7 +194,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 22000 + goodEnough := version >= 23000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index 376a6cc2..717b9929 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.12.5 (date: 9.5.2022) + +- micromamba upgrade to v0.23.0 + ## v11.12.4 (date: 9.5.2022) - bugfix: rcc task script could not find any task (reason: internal quoting) From b512b3f2570eefc27e4e5008a7ad59c289e6bad9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 10 May 2022 11:45:04 +0300 Subject: [PATCH 260/516] BUGFIX: adding ht/lib directory for mounting (v11.12.6) --- common/variables.go | 2 +- common/version.go | 2 +- docs/README.md | 57 +++++++++++++++++++++++---------------------- docs/changelog.md | 8 ++++++- docs/recipes.md | 20 ++++++++++++++++ 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/common/variables.go b/common/variables.go index 4a9f1ecd..9c5dce84 100644 --- a/common/variables.go +++ b/common/variables.go @@ -138,7 +138,7 @@ func HolotreeLocation() string { func HololibLocation() string { if FixedHolotreeLocation() { - return HoloLocation() + return filepath.Join(HoloLocation(), "lib") } return filepath.Join(RobocorpHome(), "hololib") } diff --git a/common/version.go b/common/version.go index 7159e044..3954f3ef 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.5` + Version = `v11.12.6` ) diff --git a/docs/README.md b/docs/README.md index e714e435..894229b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,35 +22,36 @@ #### 3.6.2 [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robotyaml) #### 3.6.3 [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-condayaml) #### 3.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-buildersh) -### 3.7 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) -#### 3.7.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) -#### 3.7.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### 3.8 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) -### 3.9 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### 3.9.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### 3.10 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) -#### 3.10.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.10.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### 3.10.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.10.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) -#### 3.10.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.10.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.10.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.10.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.10.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.10.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.10.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### 3.11 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +### 3.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#think-what-you-can-do-with-this-condayaml) +### 3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) +#### 3.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) +#### 3.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) +### 3.9 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### 3.10 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### 3.10.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### 3.11 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) #### 3.11.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.11.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) -#### 3.11.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### 3.11.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### 3.11.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.12 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.13 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.13.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.13.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.14 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +#### 3.11.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### 3.11.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.11.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.11.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.11.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.11.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.11.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.11.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.11.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.11.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### 3.12 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### 3.12.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.12.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### 3.12.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### 3.12.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### 3.12.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### 3.13 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.14 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.14.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.14.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.15 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) diff --git a/docs/changelog.md b/docs/changelog.md index 717b9929..868bd596 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,12 @@ # rcc change log -## v11.12.5 (date: 9.5.2022) +## v11.12.6 (date: 10.5.2022) UNSTABLE + +- bugfix: added additional directory for hololib, since it helps mounting + on servers +- one recipe addition, for idea generation ... + +## v11.12.5 (date: 9.5.2022) UNSTABLE - micromamba upgrade to v0.23.0 diff --git a/docs/recipes.md b/docs/recipes.md index cb11d24e..12fe2e32 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -266,6 +266,26 @@ cp target/build/micromamba output/micromamba-$version ``` +## Think what you can do with this conda.yaml? + +``` +channels: + # Just using conda-forge, nothing else. + - conda-forge + +dependencies: + # I'm not going to have python directly installed here .. + # But let's go wild with conda-forge ... + + - nginx=1.21.6 # https://anaconda.org/conda-forge/nginx + - php=8.1.5 # https://anaconda.org/conda-forge/php + - go=1.17.8 # https://anaconda.org/conda-forge/go + - postgresql=14.2 # https://anaconda.org/conda-forge/postgresql + - terraform=1.1.9 # https://anaconda.org/conda-forge/terraform + - awscli=1.23.9 # https://anaconda.org/conda-forge/awscli + - firefox=100.0 # https://anaconda.org/conda-forge/firefox +``` + ## How to control holotree environments? There is three controlling factors for where holotree spaces are created. From 617f18006ab3d8994f0ad471328ba8b915d0032e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 12 May 2022 14:11:30 +0300 Subject: [PATCH 261/516] UPGRADE: micromamba to v0.23.1 (v11.12.7) --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 25 +++++++++++++++++++++++++ pathlib/functions.go | 15 +++++++++++++-- 8 files changed, 48 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index 3954f3ef..6e89ffd7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.6` + Version = `v11.12.7` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 0d9e93e8..f9e81ef6 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.1/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 9c15aa4a..05a81001 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.1/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index f1fbcc2e..bfc0f97f 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.23.1/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 39b041b0..bc91a4fb 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -194,7 +194,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 23000 + goodEnough := version >= 23001 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index 868bd596..ff0ea2e6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.12.7 (date: 12.5.2022) UNSTABLE + +- micromamba upgrade to v0.23.1 +- added checks for hololib shared locations mode requirements + ## v11.12.6 (date: 10.5.2022) UNSTABLE - bugfix: added additional directory for hololib, since it helps mounting diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 1eee072a..04b976ea 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -102,6 +102,12 @@ func RunDiagnostics() *common.DiagnosticStatus { } // checks + if common.FixedHolotreeLocation() { + result.Checks = append(result.Checks, verifySharedDirectory(common.HoloLocation())) + result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLocation())) + result.Checks = append(result.Checks, verifySharedDirectory(common.HololibCatalogLocation())) + result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLibraryLocation())) + } result.Checks = append(result.Checks, robocorpHomeCheck()) result.Checks = append(result.Checks, anyPathCheck("PYTHONPATH")) result.Checks = append(result.Checks, anyPathCheck("PLAYWRIGHT_BROWSERS_PATH")) @@ -166,6 +172,25 @@ func anyPathCheck(key string) *common.DiagnosticCheck { } } +func verifySharedDirectory(fullpath string) *common.DiagnosticCheck { + shared := pathlib.IsSharedDir(fullpath) + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + if !shared { + return &common.DiagnosticCheck{ + Type: "OS", + Status: statusWarning, + Message: fmt.Sprintf("%q is not shared. This may cause problems.", fullpath), + Link: supportGeneralUrl, + } + } + return &common.DiagnosticCheck{ + Type: "OS", + Status: statusOk, + Message: fmt.Sprintf("%q is shared, which is ok.", fullpath), + Link: supportGeneralUrl, + } +} + func robocorpHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { diff --git a/pathlib/functions.go b/pathlib/functions.go index 382ba249..623d3dfc 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -49,9 +49,12 @@ func Modtime(pathname string) (time.Time, error) { return stat.ModTime(), nil } +func hasCorrectMode(stat fs.FileInfo, expected fs.FileMode) bool { + return expected == (stat.Mode() & expected) +} + func ensureCorrectMode(fullpath string, stat fs.FileInfo, correct fs.FileMode) (string, error) { - mode := stat.Mode() & correct - if mode == correct { + if hasCorrectMode(stat, correct) { return fullpath, nil } err := os.Chmod(fullpath, correct) @@ -90,6 +93,14 @@ func MakeSharedDir(fullpath string) (string, error) { return makeModedDir(fullpath, 0777) } +func IsSharedDir(fullpath string) bool { + stat, err := os.Stat(fullpath) + if err != nil { + return false + } + return stat.IsDir() && hasCorrectMode(stat, 0777) +} + func doEnsureDirectory(directory string, mode fs.FileMode) (string, error) { fullpath, err := filepath.Abs(directory) if err != nil { From a275a2a481c474e1241c75f02c6ec0ec0c2b05b3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 16 May 2022 16:24:13 +0300 Subject: [PATCH 262/516] BUGFIX: private/shared folder detection (v11.12.8) - bugfix: making shared directories shared only when they really are - new command `rcc holotree shared --enable` to enable shared holotrees in specific machine - command `rcc holotree init` is now for normal users after shared command --- cmd/command_darwin.go | 7 +++++++ cmd/command_linux.go | 18 ++++++++++++++++++ cmd/command_windows.go | 7 +++++++ cmd/holotreeInit.go | 36 ++++++++++++++++++++++++++++++------ cmd/holotreeShared.go | 35 +++++++++++++++++++++++++++++++++++ cmd/rcc/main.go | 6 +++++- common/variables.go | 25 ++++++++++++++++++++----- common/version.go | 2 +- docs/changelog.md | 7 +++++++ htfs/commands.go | 6 +++++- operations/diagnostics.go | 2 +- pathlib/copyfile.go | 2 +- pathlib/functions.go | 12 +++++++----- pathlib/lock_unix.go | 2 +- pathlib/lock_windows.go | 2 +- pathlib/shared.go | 35 +++++++++++++++++++++++++++++++++++ pathlib/variables.go | 15 +++++++++++++++ 17 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 cmd/command_darwin.go create mode 100644 cmd/command_linux.go create mode 100644 cmd/command_windows.go create mode 100644 cmd/holotreeShared.go create mode 100644 pathlib/shared.go diff --git a/cmd/command_darwin.go b/cmd/command_darwin.go new file mode 100644 index 00000000..49852d96 --- /dev/null +++ b/cmd/command_darwin.go @@ -0,0 +1,7 @@ +package cmd + +import "github.com/robocorp/rcc/pretty" + +func osSpecificHolotreeSharing(enable bool) { + pretty.Warning("Good to go. Nothing to do on Mac OS.") +} diff --git a/cmd/command_linux.go b/cmd/command_linux.go new file mode 100644 index 00000000..88bc11a3 --- /dev/null +++ b/cmd/command_linux.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" +) + +func osSpecificHolotreeSharing(enable bool) { + pathlib.ForceShared() + parent := filepath.Dir(common.HoloLocation()) + _, err := pathlib.ForceSharedDir(parent) + pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) + _, err = pathlib.ForceSharedDir(common.HoloLocation()) + pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.HoloLocation(), err) +} diff --git a/cmd/command_windows.go b/cmd/command_windows.go new file mode 100644 index 00000000..0efc44f9 --- /dev/null +++ b/cmd/command_windows.go @@ -0,0 +1,7 @@ +package cmd + +import "github.com/robocorp/rcc/pretty" + +func osSpecificHolotreeSharing(enable bool) { + pretty.Warning("Good to go. Nothing to do on Windows.") +} diff --git a/cmd/holotreeInit.go b/cmd/holotreeInit.go index bfb4cd93..95580c48 100644 --- a/cmd/holotreeInit.go +++ b/cmd/holotreeInit.go @@ -10,24 +10,48 @@ import ( "github.com/spf13/cobra" ) +var ( + revokeInit bool +) + +func disableHolotreeSharing() { + pretty.Guard(common.SharedHolotree, 5, "Not using shared holotree. Cannot disable either.") + err := os.Remove(common.HoloInitUserFile()) + pretty.Guard(err == nil, 6, "Could not remove shared user file at %q, reason: %v", common.HoloInitUserFile(), err) +} + +func enableHolotreeSharing() { + pathlib.ForceShared() + _, err := pathlib.ForceSharedDir(common.HoloInitLocation()) + pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", common.HoloInitLocation(), err) + err = os.WriteFile(common.HoloInitCommonFile(), []byte("OK!"), 0o666) + pretty.Guard(err == nil, 2, "Could not write shared common file at %q, reason: %v", common.HoloInitCommonFile(), err) + err = os.WriteFile(common.HoloInitUserFile(), []byte("OK!"), 0o640) + pretty.Guard(err == nil, 3, "Could not write shared user file at %q, reason: %v", common.HoloInitUserFile(), err) + _, err = pathlib.MakeSharedFile(common.HoloInitCommonFile()) + pretty.Guard(err == nil, 4, "Could not make shared common file actually shared at %q, reason: %v", common.HoloInitCommonFile(), err) +} + var holotreeInitCmd = &cobra.Command{ Use: "init", Short: "Initialize shared holotree location.", Long: "Initialize shared holotree location.", Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { - defer common.Stopwatch("Conda YAML hash calculation lasted").Report() + defer common.Stopwatch("Initialize shared holotree location lasted").Report() } - pretty.Guard(common.FixedHolotreeLocation(), 1, "Fixed Holotree is not available in this system!") - if os.Geteuid() > 0 { - pretty.Warning("Running this command might need sudo/root access rights. Still, trying ...") + pretty.Warning("Running this command might need 'rcc holotree shared --enable' first. Still, trying ...") + pretty.Guard(os.Geteuid() > 0, 9, "Do not run _this_ command as root. This is for normal users only.") + if revokeInit { + disableHolotreeSharing() + } else { + enableHolotreeSharing() } - _, err := pathlib.MakeSharedDir(common.HoloLocation()) - pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.HoloLocation(), err) pretty.Ok() }, } func init() { + holotreeInitCmd.Flags().BoolVarP(&revokeInit, "revoke", "r", false, "Revoke shared holotree usage. Go back to private holotree usage.") holotreeCmd.AddCommand(holotreeInitCmd) } diff --git a/cmd/holotreeShared.go b/cmd/holotreeShared.go new file mode 100644 index 00000000..ecfa92da --- /dev/null +++ b/cmd/holotreeShared.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "os" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + enableShared bool +) + +var holotreeSharedCommand = &cobra.Command{ + Use: "shared", + Short: "Enable shared holotree usage.", + Long: "Enable shared holotree usage.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Enabling shared holotree lasted").Report() + } + if os.Geteuid() > 0 { + pretty.Warning("Running this command might need sudo/root access rights. Still, trying ...") + } + osSpecificHolotreeSharing(enableShared) + pretty.Ok() + }, +} + +func init() { + holotreeSharedCommand.Flags().BoolVarP(&enableShared, "enable", "e", false, "Enable shared holotree environments between users. Currently cannot be undone.") + holotreeCmd.AddCommand(holotreeSharedCommand) +} diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index ff00bf83..d786369d 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -89,7 +89,11 @@ func markTempForRecycling() { func main() { defer ExitProtection() - common.TimelineBegin("Start.") + if common.SharedHolotree { + common.TimelineBegin("Start [shared mode].") + } else { + common.TimelineBegin("Start [private mode].") + } defer common.EndOfTimeline() go startTempRecycling() defer markTempForRecycling() diff --git a/common/variables.go b/common/variables.go index 9c5dce84..cfab4c38 100644 --- a/common/variables.go +++ b/common/variables.go @@ -13,7 +13,6 @@ import ( ) const ( - FIXED_HOLOTREE = `FIXED_HOLOTREE` ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` @@ -25,6 +24,7 @@ var ( TraceFlag bool DeveloperFlag bool StrictFlag bool + SharedHolotree bool LogLinenumbers bool NoCache bool NoOutputCapture bool @@ -53,6 +53,8 @@ func init() { // Also: HolotreeLocation creation is left for actual holotree commands // to prevent accidental access right problem during usage + SharedHolotree = isFile(HoloInitUserFile()) + ensureDirectory(TemplateLocation()) ensureDirectory(BinLocation()) ensureDirectory(PipCache()) @@ -125,19 +127,27 @@ func HoloLocation() string { return ExpandPath(defaultHoloLocation) } -func FixedHolotreeLocation() bool { - return len(os.Getenv(FIXED_HOLOTREE)) > 0 +func HoloInitLocation() string { + return filepath.Join(HoloLocation(), "lib", "catalog", "init") +} + +func HoloInitUserFile() string { + return filepath.Join(HoloInitLocation(), UserHomeIdentity()) +} + +func HoloInitCommonFile() string { + return filepath.Join(HoloInitLocation(), "commons.tof") } func HolotreeLocation() string { - if FixedHolotreeLocation() { + if SharedHolotree { return HoloLocation() } return filepath.Join(RobocorpHome(), "holotree") } func HololibLocation() string { - if FixedHolotreeLocation() { + if SharedHolotree { return filepath.Join(HoloLocation(), "lib") } return filepath.Join(RobocorpHome(), "hololib") @@ -229,6 +239,11 @@ func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } +func isFile(pathname string) bool { + stat, err := os.Stat(pathname) + return err == nil && stat.Mode().IsRegular() +} + func isDir(pathname string) bool { stat, err := os.Stat(pathname) return err == nil && stat.IsDir() diff --git a/common/version.go b/common/version.go index 6e89ffd7..36aa1564 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.7` + Version = `v11.12.8` ) diff --git a/docs/changelog.md b/docs/changelog.md index ff0ea2e6..c13c202e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.12.8 (date: 16.5.2022) UNSTABLE + +- bugfix: making shared directories shared only when they really are +- new command `rcc holotree shared --enable` to enable shared holotrees + in specific machine +- command `rcc holotree init` is now for normal users after shared command + ## v11.12.7 (date: 12.5.2022) UNSTABLE - micromamba upgrade to v0.23.1 diff --git a/htfs/commands.go b/htfs/commands.go index fd398096..b9d96ed1 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -30,7 +30,11 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin pretty.Note("There is hololib.zip present at: %q", holozip) } }() - common.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) + if common.SharedHolotree { + common.Progress(1, "Fresh [shared mode] holotree environment %v.", xviper.TrackingIdentity()) + } else { + common.Progress(1, "Fresh [private mode] holotree environment %v.", xviper.TrackingIdentity()) + } callback := pathlib.LockWaitMessage("Serialized environment creation") locker, err := pathlib.Locker(common.HolotreeLock(), 30000) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 04b976ea..21fa436d 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -102,7 +102,7 @@ func RunDiagnostics() *common.DiagnosticStatus { } // checks - if common.FixedHolotreeLocation() { + if common.SharedHolotree { result.Checks = append(result.Checks, verifySharedDirectory(common.HoloLocation())) result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLocation())) result.Checks = append(result.Checks, verifySharedDirectory(common.HololibCatalogLocation())) diff --git a/pathlib/copyfile.go b/pathlib/copyfile.go index 5148c190..c12c2e44 100644 --- a/pathlib/copyfile.go +++ b/pathlib/copyfile.go @@ -23,7 +23,7 @@ func CopyFile(source, target string, overwrite bool) error { } func copyFile(source, target string, overwrite bool, copier copyfunc) error { - _, err := MakeSharedDir(filepath.Dir(target)) + _, err := shared.MakeSharedDir(filepath.Dir(target)) if err != nil { return err } diff --git a/pathlib/functions.go b/pathlib/functions.go index 623d3dfc..3299ebef 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -72,7 +72,7 @@ func makeModedDir(fullpath string, correct fs.FileMode) (path string, err error) return ensureCorrectMode(fullpath, stat, correct) } fail.On(err == nil, "Path %q exists, but is not a directory!", fullpath) - _, err = MakeSharedDir(filepath.Dir(fullpath)) + _, err = shared.MakeSharedDir(filepath.Dir(fullpath)) fail.On(err != nil, "%v", err) err = os.Mkdir(fullpath, correct) fail.On(err != nil, "Failed to create directory %q, reason: %v", fullpath, err) @@ -84,12 +84,14 @@ func makeModedDir(fullpath string, correct fs.FileMode) (path string, err error) } func MakeSharedFile(fullpath string) (string, error) { - stat, err := os.Stat(fullpath) - fail.On(err != nil, "Failed to stat file %q, reason: %v", fullpath, err) - return ensureCorrectMode(fullpath, stat, 0666) + return shared.MakeSharedFile(fullpath) } func MakeSharedDir(fullpath string) (string, error) { + return shared.MakeSharedDir(fullpath) +} + +func ForceSharedDir(fullpath string) (string, error) { return makeModedDir(fullpath, 0777) } @@ -121,7 +123,7 @@ func doEnsureDirectory(directory string, mode fs.FileMode) (string, error) { } func EnsureSharedDirectory(directory string) (string, error) { - return MakeSharedDir(directory) + return shared.MakeSharedDir(directory) } func EnsureSharedParentDirectory(resource string) (string, error) { diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index d3e9959b..83d5dd6a 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -26,7 +26,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } - _, err = MakeSharedFile(filename) + _, err = shared.MakeSharedFile(filename) if err != nil { return nil, err } diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 1d56dcd7..6c2f05f6 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -54,7 +54,7 @@ func Locker(filename string, trycount int) (Releaser, error) { time.Sleep(40 * time.Millisecond) continue } - _, err = MakeSharedFile(filename) + _, err = shared.MakeSharedFile(filename) if err != nil { return nil, err } diff --git a/pathlib/shared.go b/pathlib/shared.go new file mode 100644 index 00000000..6dfa2067 --- /dev/null +++ b/pathlib/shared.go @@ -0,0 +1,35 @@ +package pathlib + +import ( + "os" + + "github.com/robocorp/rcc/fail" +) + +type ( + Shared interface { + MakeSharedFile(fullpath string) (string, error) + MakeSharedDir(fullpath string) (string, error) + } + + privateSetup uint8 + sharedSetup uint8 +) + +func (it privateSetup) MakeSharedFile(fullpath string) (string, error) { + return fullpath, nil +} + +func (it privateSetup) MakeSharedDir(fullpath string) (string, error) { + return makeModedDir(fullpath, 0750) +} + +func (it sharedSetup) MakeSharedFile(fullpath string) (string, error) { + stat, err := os.Stat(fullpath) + fail.On(err != nil, "Failed to stat file %q, reason: %v", fullpath, err) + return ensureCorrectMode(fullpath, stat, 0666) +} + +func (it sharedSetup) MakeSharedDir(fullpath string) (string, error) { + return makeModedDir(fullpath, 0777) +} diff --git a/pathlib/variables.go b/pathlib/variables.go index 0f6b7771..71360728 100644 --- a/pathlib/variables.go +++ b/pathlib/variables.go @@ -1,5 +1,20 @@ package pathlib +import "github.com/robocorp/rcc/common" + var ( Lockless bool + shared Shared ) + +func init() { + if common.SharedHolotree { + ForceShared() + } else { + shared = privateSetup(1) + } +} + +func ForceShared() { + shared = sharedSetup(9) +} From f4565d1d8ea2a98a3bced9704fdca9e0ff8d4e73 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 17 May 2022 11:27:28 +0300 Subject: [PATCH 263/516] BUGFIX: effective user id fix (v11.12.9) - bugfix: effective user id did not work on windows, removing it for all OSs - diagnostics now has true/false flag to indicated shared/private holotrees --- cmd/holotreeInit.go | 1 - common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/holotreeInit.go b/cmd/holotreeInit.go index 95580c48..01a19a53 100644 --- a/cmd/holotreeInit.go +++ b/cmd/holotreeInit.go @@ -41,7 +41,6 @@ var holotreeInitCmd = &cobra.Command{ defer common.Stopwatch("Initialize shared holotree location lasted").Report() } pretty.Warning("Running this command might need 'rcc holotree shared --enable' first. Still, trying ...") - pretty.Guard(os.Geteuid() > 0, 9, "Do not run _this_ command as root. This is for normal users only.") if revokeInit { disableHolotreeSharing() } else { diff --git a/common/version.go b/common/version.go index 36aa1564..586294c2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.8` + Version = `v11.12.9` ) diff --git a/docs/changelog.md b/docs/changelog.md index c13c202e..9e4bed80 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.12.9 (date: 17.5.2022) UNSTABLE + +- bugfix: effective user id did not work on windows, removing it for all OSs +- diagnostics now has true/false flag to indicated shared/private holotrees + ## v11.12.8 (date: 16.5.2022) UNSTABLE - bugfix: making shared directories shared only when they really are diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 21fa436d..7075683e 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -91,6 +91,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["hololib-catalog-location"] = common.HololibCatalogLocation() result.Details["hololib-library-location"] = common.HololibLibraryLocation() result.Details["holotree-location"] = common.HolotreeLocation() + result.Details["holotree-shared"] = fmt.Sprintf("%v", common.SharedHolotree) result.Details["holotree-user-id"] = common.UserHomeIdentity() result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) From 47eed71ddaeee74d91bd2d497211920720e320fd Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 19 May 2022 11:28:35 +0300 Subject: [PATCH 264/516] FEATURE: shared holotree released (v11.13.0) - new shared holotree should now be effective - some instructions on recipes for enabling shared holotree - micromamba upgrade to v0.23.2 --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/README.md | 57 ++++++++++++++++++--------------- docs/changelog.md | 6 ++++ docs/recipes.md | 35 ++++++++++++++++++++ 8 files changed, 77 insertions(+), 31 deletions(-) diff --git a/common/version.go b/common/version.go index 586294c2..b3d2cdf1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.12.9` + Version = `v11.13.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index f9e81ef6..3580cf29 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.1/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.2/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 05a81001..310cb3bf 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.1/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.23.2/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index bfc0f97f..72bec631 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.1/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.23.2/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index bc91a4fb..b9464267 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -194,7 +194,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 23001 + goodEnough := version >= 23002 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/README.md b/docs/README.md index 894229b9..ee7061fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,32 +26,37 @@ ### 3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) #### 3.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) #### 3.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### 3.9 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) -### 3.10 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### 3.10.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### 3.11 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) -#### 3.11.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.11.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### 3.11.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.11.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) -#### 3.11.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.11.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.11.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.11.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.11.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.11.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.11.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### 3.12 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) -#### 3.12.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.12.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) -#### 3.12.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### 3.12.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### 3.12.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.13 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.14 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.14.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.14.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.15 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +### 3.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree) +### 3.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree) +#### 3.10.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#one-time-setup) +#### 3.10.2 [Per user initialization](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#per-user-initialization) +#### 3.10.3 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#reverting-back-to-private-holotrees) +### 3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### 3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### 3.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### 3.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) +#### 3.13.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.13.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### 3.13.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.13.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.13.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.13.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.13.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.13.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.13.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.13.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.13.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### 3.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### 3.14.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.14.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### 3.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### 3.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### 3.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### 3.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.16.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.16.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) diff --git a/docs/changelog.md b/docs/changelog.md index 9e4bed80..41658d7a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.13.0 (date: 19.5.2022) + +- new shared holotree should now be effective +- some instructions on recipes for enabling shared holotree +- micromamba upgrade to v0.23.2 + ## v11.12.9 (date: 17.5.2022) UNSTABLE - bugfix: effective user id did not work on windows, removing it for all OSs diff --git a/docs/recipes.md b/docs/recipes.md index 12fe2e32..896c7785 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -360,6 +360,41 @@ rcc task shell --robot path/to/robot.yaml ``` +## What is shared holotree? + +Shared holotree is way to multiple users use same environment blueprint in +same machine, or even in different machines with same, once it is built or +imported into hololib. + +## How to setup rcc to use shared holotree? + +### One time setup + +On each machine, where you want to use shared holotree, you needs once to +enable using it. It probably needs elevated rights to run, if operating system +limits your access to shared resource. You can do it following way. + +```sh +sudo rcc holotree shared --enable +``` + +### Per user initialization + +If user wants to use shared holotrees, then they have to initialize to use +those shared settings. That can be done using following command. + +```sh +rcc holotree init +``` + +### Reverting back to private holotrees + +If user wants to go back to private holotrees, they can run following command. + +```sh +rcc holotree init --revoke +``` + ## What can be controlled using environment variables? - `ROBOCORP_HOME` points to directory where rcc keeps most of Robocorp related From 687cded241c1a53ee75e9d06922cd94bf7b9fab8 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Tue, 7 Jun 2022 17:39:42 +0300 Subject: [PATCH 265/516] Testing CodeQL scanning workflow --- .github/workflows/codeql-analysis.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..8c78014a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '24 10 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'python', 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From af93eb2e240f3b6f99342fc0e31e42f703b73e9d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Jun 2022 17:06:55 +0300 Subject: [PATCH 266/516] FEATURE: setting VIRTUAL_ENV variable (v11.14.0) - experimenting on setting `VIRTUAL_ENV` environment variable to point into environment rcc created environment - made OS and architecture visible in rcc "Progress 2" marker --- common/version.go | 2 +- conda/robocorp.go | 3 +++ docs/changelog.md | 6 ++++++ htfs/commands.go | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index b3d2cdf1..dd1a2e52 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.13.0` + Version = `v11.14.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index b9464267..50090e72 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -101,8 +101,10 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if !ok { python, ok = holotreePath.Which("python", FileExtensions) } + virtualenv := "" if ok { environment = append(environment, "PYTHON_EXE="+python) + virtualenv = location } environment = append(environment, "CONDA_DEFAULT_ENV=rcc", @@ -112,6 +114,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "PYTHONHOME=", "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", + "VIRTUAL_ENV="+virtualenv, "PYTHONNOUSERSITE=1", "PYTHONDONTWRITEBYTECODE=x", "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), diff --git a/docs/changelog.md b/docs/changelog.md index 41658d7a..bbb435f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.14.0 (date: 7.6.2022) + +- experimenting on setting `VIRTUAL_ENV` environment variable to point into + environment rcc created environment +- made OS and architecture visible in rcc "Progress 2" marker + ## v11.13.0 (date: 19.5.2022) - new shared holotree should now be effective diff --git a/htfs/commands.go b/htfs/commands.go index b9d96ed1..74b4986e 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -45,7 +45,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash = BlueprintHash(holotreeBlueprint) - common.Progress(2, "Holotree blueprint is %q.", common.EnvironmentHash) + common.Progress(2, "Holotree blueprint is %q [%s].", common.EnvironmentHash, common.Platform()) tree, err := New() fail.On(err != nil, "%s", err) From b482ca7a59dd9150fc6860a2d6620a57561de7aa Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Jun 2022 10:15:22 +0300 Subject: [PATCH 267/516] BUGFIX: codeql findings fix (v11.14.1) - fixing codeql-analysis settings and problems - no codeql analysis for ruby or python in this repo --- .github/workflows/codeql-analysis.yml | 6 +++--- common/version.go | 2 +- conda/librarian.go | 6 ++++-- docs/changelog.md | 5 +++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8c78014a..438a3150 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go', 'python', 'ruby' ] + language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support @@ -48,11 +48,11 @@ jobs: # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild diff --git a/common/version.go b/common/version.go index dd1a2e52..5d7d0287 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.0` + Version = `v11.14.1` ) diff --git a/conda/librarian.go b/conda/librarian.go index 44769851..bc9b4c1c 100644 --- a/conda/librarian.go +++ b/conda/librarian.go @@ -42,7 +42,8 @@ func Index(search string, members []string) int { } func updateChannels(environment *Environment, changes *Changes) { - result := make([]string, 0, len(changes.Add)+len(environment.Channels)) + predicted := uint64(len(changes.Add) + len(environment.Channels)) + result := make([]string, 0, predicted) for _, current := range environment.Channels { if Index(current, changes.Remove) > -1 { continue @@ -78,7 +79,8 @@ func updatePackages(environment *Environment, changes *Changes) error { } func composePackages(target []*Dependency, add []*Dependency, remove []*Dependency) ([]*Dependency, error) { - result := make([]*Dependency, 0, len(target)+len(add)) + predicted := uint64(len(target) + len(add)) + result := make([]*Dependency, 0, predicted) for _, current := range target { if current.Index(remove) > -1 { continue diff --git a/docs/changelog.md b/docs/changelog.md index bbb435f2..d8d425e9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.14.1 (date: 8.6.2022) + +- fixing codeql-analysis settings and problems +- no codeql analysis for ruby or python in this repo + ## v11.14.0 (date: 7.6.2022) - experimenting on setting `VIRTUAL_ENV` environment variable to point into From 2072762593c46170dd043d7e3f844120f19a796c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Jun 2022 11:51:05 +0300 Subject: [PATCH 268/516] BUGFIX: codeql findings fix retry (v11.14.2) - retry on fixing codeql-analysis problem --- common/version.go | 2 +- conda/librarian.go | 8 ++++++-- docs/changelog.md | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 5d7d0287..ea327ac4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.1` + Version = `v11.14.2` ) diff --git a/conda/librarian.go b/conda/librarian.go index bc9b4c1c..a326dd88 100644 --- a/conda/librarian.go +++ b/conda/librarian.go @@ -41,8 +41,12 @@ func Index(search string, members []string) int { return -1 } +func bitGuard(size int) uint64 { + return uint64(size & 0x0000_3fff_ffff_ffff) +} + func updateChannels(environment *Environment, changes *Changes) { - predicted := uint64(len(changes.Add) + len(environment.Channels)) + predicted := uint64(bitGuard(len(changes.Add)) + bitGuard(len(environment.Channels))) result := make([]string, 0, predicted) for _, current := range environment.Channels { if Index(current, changes.Remove) > -1 { @@ -79,7 +83,7 @@ func updatePackages(environment *Environment, changes *Changes) error { } func composePackages(target []*Dependency, add []*Dependency, remove []*Dependency) ([]*Dependency, error) { - predicted := uint64(len(target) + len(add)) + predicted := uint64(bitGuard(len(target)) + bitGuard(len(add))) result := make([]*Dependency, 0, predicted) for _, current := range target { if current.Index(remove) > -1 { diff --git a/docs/changelog.md b/docs/changelog.md index d8d425e9..ff48d252 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.14.2 (date: 8.6.2022) + +- retry on fixing codeql-analysis problem + ## v11.14.1 (date: 8.6.2022) - fixing codeql-analysis settings and problems From ad1b757796cdcc221bb5d6c2490ada4553c14763 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 9 Jun 2022 12:59:49 +0300 Subject: [PATCH 269/516] UPGRADE: using go v1.18.x (v11.14.3) --- .github/workflows/rcc.yaml | 4 ++-- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index d0fd6de7..10d94223 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: '1.17.x' + go-version: '1.18.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: '1.17.x' + go-version: '1.18.x' - uses: actions/setup-ruby@v1 with: ruby-version: '2.5' diff --git a/common/version.go b/common/version.go index ea327ac4..13916820 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.2` + Version = `v11.14.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index ff48d252..952abee0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.14.3 (date: 9.6.2022) + +- upgraded rcc to be build using go v1.18.x + ## v11.14.2 (date: 8.6.2022) - retry on fixing codeql-analysis problem From e57621aad109666cb2b9dae7cda5037a4a834c47 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 9 Jun 2022 14:59:26 +0300 Subject: [PATCH 270/516] Updated guides for shared holotree activation --- docs/README.md | 346 +++++++++++++++++++++++++++++++++++++++++++++++- docs/recipes.md | 28 ++-- 2 files changed, 359 insertions(+), 15 deletions(-) diff --git a/docs/README.md b/docs/README.md index ee7061fe..7af6812b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,8 +29,7 @@ ### 3.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree) ### 3.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree) #### 3.10.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#one-time-setup) -#### 3.10.2 [Per user initialization](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#per-user-initialization) -#### 3.10.3 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#reverting-back-to-private-holotrees) +#### 3.10.2 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#reverting-back-to-private-holotrees) ### 3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) ### 3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) #### 3.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) @@ -71,7 +70,342 @@ #### 4.7.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) #### 4.7.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) #### 4.7.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it) -### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) -### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) -### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file +## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#rcc----how-to-build-it) +### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#tooling) +### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#commands) +### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#where-to-start-reading-code) +## 6 [rcc change log](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#rcc-change-log) +### 6.1 [v11.14.3 (date: 9.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.2 [v11.14.2 (date: 8.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.3 [v11.14.1 (date: 8.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.4 [v11.14.0 (date: 7.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.5 [v11.13.0 (date: 19.5.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.6 [v11.12.9 (date: 17.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.7 [v11.12.8 (date: 16.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.8 [v11.12.7 (date: 12.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.9 [v11.12.6 (date: 10.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.10 [v11.12.5 (date: 9.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.11 [v11.12.4 (date: 9.5.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.12 [v11.12.3 (date: 5.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.13 [v11.12.2 (date: 5.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.14 [v11.12.1 (date: 29.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.15 [v11.12.0 (date: 28.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.16 [v11.11.2 (date: 26.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.17 [v11.11.1 (date: 26.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.18 [v11.11.0 (date: 25.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.19 [v11.10.7 (date: 22.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.20 [v11.10.6 (date: 21.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.21 [v11.10.5 (date: 20.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.22 [v11.10.4 (date: 20.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.23 [v11.10.3 (date: 19.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.24 [v11.10.2 (date: 13.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.25 [v11.10.1 (date: 12.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.26 [v11.10.0 (date: 12.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.27 [v11.9.16 (date: 7.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.28 [v11.9.15 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.29 [v11.9.14 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.30 [v11.9.13 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.31 [v11.9.12 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.32 [v11.9.11 (date: 5.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.33 [v11.9.10 (date: 4.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.34 [v11.9.9 (date: 31.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.35 [v11.9.8 (date: 29.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.36 [v11.9.7 (date: 28.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.37 [v11.9.6 (date: 25.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.38 [v11.9.5 (date: 23.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.39 [v11.9.4 (date: 22.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.40 [v11.9.3 (date: 22.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.41 [v11.9.2 (date: 18.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.42 [v11.9.1 (date: 10.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.43 [v11.9.0 (date: 9.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.44 [v11.8.0 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.45 [v11.7.1 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.46 [v11.7.0 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.47 [v11.6.6 (date: 7.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.48 [v11.6.5 (date: 2.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.49 [v11.6.4 (date: 23.2.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.50 [v11.6.3 (date: 10.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.51 [v11.6.2 (date: 7.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.52 [v11.6.1 (date: 7.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.53 [v11.6.0 (date: 7.12.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.54 [v11.5.5 (date: 2.11.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.55 [v11.5.4 (date: 29.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.56 [v11.5.3 (date: 28.10.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.57 [v11.5.2 (date: 27.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.58 [v11.5.1 (date: 26.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.59 [v11.5.0 (date: 20.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.60 [v11.4.3 (date: 20.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.61 [v11.4.2 (date: 19.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.62 [v11.4.1 (date: 18.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.63 [v11.4.0 (date: 18.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.64 [v11.3.6 (date: 13.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.65 [v11.3.5 (date: 12.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.66 [v11.3.4 (date: 12.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.67 [v11.3.3 (date: 8.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.68 [v11.3.2 (date: 7.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.69 [v11.3.1 (date: 5.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.70 [v11.3.0 (date: 4.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.71 [v11.2.0 (date: 29.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.72 [v11.1.6 (date: 27.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +#### 6.72.1 [What to consider when upgrading from series 10 to series 11 of rcc?](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#what-to-consider-when-upgrading-from-series-to-series-of-rcc) +### 6.73 [v11.1.5 (date: 24.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.74 [v11.1.4 (date: 23.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.75 [v11.1.3 (date: 21.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.76 [v11.1.2 (date: 20.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.77 [v11.1.1 (date: 17.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.78 [v11.1.0 (date: 16.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.79 [v11.0.8 (date: 15.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.80 [v11.0.7 (date: 14.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.81 [v11.0.6 (date: 13.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.82 [v11.0.5 (date: 10.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.83 [v11.0.4 (date: 9.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.84 [v11.0.3 (date: 8.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.85 [v11.0.2 (date: 8.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.86 [v11.0.1 (date: 7.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.87 [v11.0.0 (date: 6.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) +### 6.88 [v10.10.0 (date: 7.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.89 [v10.9.4 (date: 31.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.90 [v10.9.3 (date: 31.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.91 [v10.9.2 (date: 30.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.92 [v10.9.1 (date: 27.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.93 [v10.9.0 (date: 25.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.94 [v10.8.1 (date: 24.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.95 [v10.8.0 (date: 19.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.96 [v10.7.1 (date: 18.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.97 [v10.7.0 (date: 16.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.98 [v10.6.0 (date: 16.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.99 [v10.5.2 (date: 12.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.100 [v10.5.1 (date: 11.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.101 [v10.5.0 (date: 10.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.102 [v10.4.5 (date: 10.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.103 [v10.4.4 (date: 9.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.104 [v10.4.3 (date: 9.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.105 [v10.4.2 (date: 5.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.106 [v10.4.1 (date: 5.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.107 [v10.4.0 (date: 5.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.108 [v10.3.3 (date: 29.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.109 [v10.3.2 (date: 29.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.110 [v10.3.1 (date: 29.6.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.111 [v10.3.0 (date: 28.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.112 [v10.2.4 (date: 24.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.113 [v10.2.3 (date: 24.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.114 [v10.2.2 (date: 23.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.115 [v10.2.1 (date: 21.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.116 [v10.2.0 (date: 21.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.117 [v10.1.1 (date: 18.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.118 [v10.1.0 (date: 17.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.119 [v10.0.0 (date: 15.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.120 [v9.20.0 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.121 [v9.19.4 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.122 [v9.19.3 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.123 [v9.19.2 (date: 9.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.124 [v9.19.1 (date: 8.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.125 [v9.19.0 (date: 8.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.126 [v9.18.0 (date: 3.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.127 [v9.17.2 (date: 2.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.128 [v9.17.1 (date: 2.6.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.129 [v9.17.0 (date: 26.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.130 [v9.16.0 (date: 21.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.131 [v9.15.1 (date: 21.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.132 [v9.15.0 (date: 20.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.133 [v9.14.0 (date: 19.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.134 [v9.13.0 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.135 [v9.12.1 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.136 [v9.12.0 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.137 [v9.11.3 (date: 12.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.138 [v9.11.2 (date: 11.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.139 [v9.11.1 (date: 7.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.140 [v9.11.0 (date: 6.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.141 [v9.10.2 (date: 5.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.142 [v9.10.1 (date: 5.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.143 [v9.10.0 (date: 4.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.144 [v9.9.21 (date: 4.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.145 [v9.9.20 (date: 3.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.146 [v9.9.19 (date: 29.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.147 [v9.9.18 (date: 28.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.148 [v9.9.17 (date: 20.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.149 [v9.9.16 (date: 20.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.150 [v9.9.15 (date: 19.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.151 [v9.9.14 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.152 [v9.9.13 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.153 [v9.9.12 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.154 [v9.9.11 (date: 13.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.155 [v9.9.10 (date: 12.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.156 [v9.9.9 (date: 9.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.157 [v9.9.8 (date: 9.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.158 [v9.9.7 (date: 8.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.159 [v9.9.6 (date: 8.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.160 [v9.9.5 (date: 6.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.161 [v9.9.4 (date: 6.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.162 [v9.9.3 (date: 1.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.163 [v9.9.2 (date: 1.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.164 [v9.9.1 (date: 31.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.165 [v9.9.0 (date: 31.3.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) +### 6.166 [v9.8.11 (date: 30.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.167 [v9.8.10 (date: 30.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.168 [v9.8.9 (date: 29.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.169 [v9.8.8 (date: 29.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.170 [v9.8.7 (date: 26.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.171 [v9.8.6 (date: 25.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.172 [v9.8.5 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.173 [v9.8.4 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.174 [v9.8.3 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.175 [v9.8.2 (date: 23.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.176 [v9.8.1 (date: 22.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.177 [v9.8.0 (date: 18.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.178 [v9.7.4 (date: 17.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.179 [v9.7.3 (date: 16.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.180 [v9.7.2 (date: 11.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.181 [v9.7.1 (date: 10.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.182 [v9.7.0 (date: 10.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.183 [v9.6.2 (date: 5.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.184 [v9.6.1 (date: 3.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.185 [v9.6.0 (date: 3.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.186 [v9.5.4 (date: 2.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.187 [v9.5.3 (date: 2.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.188 [v9.5.2 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.189 [v9.5.1 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.190 [v9.5.0 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.191 [v9.4.4 (date: 24.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.192 [v9.4.3 (date: 23.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.193 [v9.4.2 (date: 23.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.194 [v9.4.1 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.195 [v9.4.0 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.196 [v9.3.12 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.197 [v9.3.11 (date: 15.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.198 [v9.3.10 (date: 11.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.199 [v9.3.9 (date: 8.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.200 [v9.3.8 (date: 4.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.201 [v9.3.7 (date: 4.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.202 [v9.3.6 (date: 3.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.203 [v9.3.5 (date: 2.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.204 [v9.3.4 (date: 1.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.205 [v9.3.3 (date: 1.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.206 [v9.3.2 (date: 29.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.207 [v9.3.1 (date: 29.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.208 [v9.3.0 (date: 28.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.209 [v9.2.0 (date: 25.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.210 [v9.1.0 (date: 25.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.211 [v9.0.2 (date: 21.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.212 [v9.0.1 (date: 20.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.213 [v9.0.0 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.214 [v8.0.12 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.215 [v8.0.10 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.216 [v8.0.9 (date: 15.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.217 [v8.0.8 (date: 15.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.218 [v8.0.7 (date: 11.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.219 [v8.0.6 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.220 [v8.0.5 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.221 [v8.0.4 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.222 [v8.0.3 (date: 7.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.223 [v8.0.2 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.224 [v8.0.1 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.225 [v8.0.0 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.226 [v7.1.5 (date: 4.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.227 [v7.1.4 (date: 4.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) +### 6.228 [Older versions](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#older-versions) +### 6.229 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#what-is-execution-environment-isolation-and-caching) +### 6.230 [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#the-second-evolution-of-environment-management-in-rcc) +#### 6.230.1 [Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#relocation-and-file-locking) +### 6.231 [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#a-better-analogy-accommodations) +#### 6.231.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#i-invited-you-to-my-home) +#### 6.231.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) +#### 6.231.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#welcome-to-an-actual-hotel) +## 7 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs\features.md#incomplete-list-of-rcc-features) +## 8 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#profile-configuration) +### 8.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-is-profile) +#### 8.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#when-do-you-need-profiles) +#### 8.1.2 [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-does-it-contain) +### 8.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#quick-start-guide) +### 8.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-is-needed) +### 8.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#discovery-process) +## 9 [Table of contents: rcc documentation](https://github.com/robocorp/rcc/blob/master/docs\README.md#table-of-contents-rcc-documentation) +### 9.1 [1 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases)](https://github.com/robocorp/rcc/blob/master/docs\README.md#incomplete-list-of-rcc-use-cases-https-githubcom-robocorp-rcc-blob-master-docs-usecasesmd-incomplete-list-of-rcc-use-cases) +#### 9.1.1 [1.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-available-from-conda-forge-https-githubcom-robocorp-rcc-blob-master-docs-usecasesmd-what-is-available-from-conda-forge) +### 9.2 [2 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features)](https://github.com/robocorp/rcc/blob/master/docs\README.md#incomplete-list-of-rcc-features-https-githubcom-robocorp-rcc-blob-master-docs-featuresmd-incomplete-list-of-rcc-features) +### 9.3 [3 [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies)](https://github.com/robocorp/rcc/blob/master/docs\README.md#tips-tricks-and-recipies-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-tips-tricks-and-recipies) +#### 9.3.1 [3.1 [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-see-dependency-changes-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-see-dependency-changes) +#### 9.3.2 [3.2 [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-freeze-dependencies-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-freeze-dependencies) +#### 9.3.3 [3.3 [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-pass-arguments-to-robot-from-cli-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-pass-arguments-to-robot-from-cli) +#### 9.3.4 [3.4 [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-run-any-command-inside-robot-environment-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-run-any-command-inside-robot-environment) +#### 9.3.5 [3.5 [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-convert-existing-python-project-to-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-convert-existing-python-project-to-rcc) +#### 9.3.6 [3.6 [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-is-rcc-limited-to-python-and-robot-framework-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-is-rcc-limited-to-python-and-robot-framework) +#### 9.3.7 [3.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#think-what-you-can-do-with-this-condayaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-think-what-you-can-do-with-this-condayaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-think-what-you-can-do-with-this-condayaml) +#### 9.3.8 [3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-control-holotree-environments-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-control-holotree-environments) +#### 9.3.9 [3.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-shared-holotree-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-shared-holotree) +#### 9.3.10 [3.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-setup-rcc-to-use-shared-holotree-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-setup-rcc-to-use-shared-holotree) +#### 9.3.11 [3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-can-be-controlled-using-environment-variables-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-can-be-controlled-using-environment-variables) +#### 9.3.12 [3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-troubleshoot-rcc-setup-and-robots-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-troubleshoot-rcc-setup-and-robots) +#### 9.3.13 [3.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-in-robotyaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-in-robotyaml) +#### 9.3.14 [3.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-in-condayaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-in-condayaml) +#### 9.3.15 [3.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-where-can-i-find-updates-for-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-where-can-i-find-updates-for-rcc) +#### 9.3.16 [3.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-has-changed-on-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-has-changed-on-rcc) +#### 9.3.17 [3.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-can-i-see-these-tips-as-web-page-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-can-i-see-these-tips-as-web-page) +### 9.4 [4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration)](https://github.com/robocorp/rcc/blob/master/docs\README.md#profile-configuration-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-profile-configuration) +#### 9.4.1 [4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-profile-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-what-is-profile) +#### 9.4.2 [4.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-quick-start-guide-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-quick-start-guide) +#### 9.4.3 [4.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-needed-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-what-is-needed) +#### 9.4.4 [4.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-discovery-process-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-discovery-process) +#### 9.4.5 [4.5 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-execution-environment-isolation-and-caching-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-what-is-execution-environment-isolation-and-caching) +#### 9.4.6 [4.6 [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-the-second-evolution-of-environment-management-in-rcc-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-the-second-evolution-of-environment-management-in-rcc) +#### 9.4.7 [4.7 [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-a-better-analogy-accommodations-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-a-better-analogy-accommodations) +### 9.5 [5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it)](https://github.com/robocorp/rcc/blob/master/docs\README.md#rcc----how-to-build-it-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-rcc----how-to-build-it) +#### 9.5.1 [5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-tooling-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-tooling) +#### 9.5.2 [5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-commands-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-commands) +#### 9.5.3 [5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-where-to-start-reading-code-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-where-to-start-reading-code) +## 10 [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#tips-tricks-and-recipies) +### 10.1 [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-see-dependency-changes) +#### 10.1.1 [Why is this important?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#why-is-this-important) +#### 10.1.2 [Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example-of-dependencies-listing-from-holotree-environment) +### 10.2 [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-freeze-dependencies) +#### 10.2.1 [Steps](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#steps) +#### 10.2.2 [Limitations](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#limitations) +### 10.3 [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-pass-arguments-to-robot-from-cli) +#### 10.3.1 [Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example-robotyaml-with-scripting-task) +#### 10.3.2 [Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#run-it-with----separator) +### 10.4 [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-run-any-command-inside-robot-environment) +#### 10.4.1 [Some example commands](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#some-example-commands) +### 10.5 [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-convert-existing-python-project-to-rcc) +#### 10.5.1 [Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#basic-workflow-to-get-it-up-and-running) +#### 10.5.2 [What next?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-next) +### 10.6 [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#is-rcc-limited-to-python-and-robot-framework) +#### 10.6.1 [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#this-is-what-we-are-going-to-do-) +#### 10.6.2 [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-robotyaml) +#### 10.6.3 [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-condayaml) +#### 10.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-bin-buildersh) +### 10.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#think-what-you-can-do-with-this-condayaml) +### 10.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-control-holotree-environments) +#### 10.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-get-understanding-on-holotree) +#### 10.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-activate-holotree-environment) +### 10.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-shared-holotree) +### 10.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-setup-rcc-to-use-shared-holotree) +#### 10.10.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#one-time-setup) +#### 10.10.2 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#reverting-back-to-private-holotrees) +### 10.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-can-be-controlled-using-environment-variables) +### 10.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### 10.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#additional-debugging-options) +### 10.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-in-robotyaml) +#### 10.13.1 [Example](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example) +#### 10.13.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-this-robotyaml-thing) +#### 10.13.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-tasks) +#### 10.13.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-devtasks) +#### 10.13.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-condaconfigfile) +#### 10.13.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-environmentconfigs) +#### 10.13.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-prerunscripts) +#### 10.13.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-artifactsdir) +#### 10.13.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-ignorefiles) +#### 10.13.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-path) +#### 10.13.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-pythonpath) +### 10.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-in-condayaml) +#### 10.14.1 [Example](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example) +#### 10.14.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-this-condayaml-thing) +#### 10.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-channels) +#### 10.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-dependencies) +#### 10.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-rccpostinstall-scripts) +### 10.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#where-can-i-find-updates-for-rcc) +### 10.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-has-changed-on-rcc) +#### 10.16.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#see-changelog-from-git-repo-) +#### 10.16.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#see-that-from-your-version-of-rcc-directly-) +### 10.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#can-i-see-these-tips-as-web-page) +## 11 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs\usecases.md#incomplete-list-of-rcc-use-cases) +### 11.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs\usecases.md#what-is-available-from-conda-forge) \ No newline at end of file diff --git a/docs/recipes.md b/docs/recipes.md index 896c7785..70846879 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -370,18 +370,28 @@ imported into hololib. ### One time setup -On each machine, where you want to use shared holotree, you needs once to -enable using it. It probably needs elevated rights to run, if operating system -limits your access to shared resource. You can do it following way. +On each machine, where you want to use shared holotree, the shared location +needs to be enabled once. +This depends on the operating system so the commands below are OS specific +and do require elevated rights from the user that runs them. + +The commands to enable the shared locations are: +* Windows: `rcc holotree shared --enable` + * Shared location: `C:\ProgramData\robocorp` +* MacOS: `sudo rcc holotree shared --enable` + * Shared location: `/Users/Shared/robocorp` +* Linux: `sudo rcc holotree shared --enable` + * Shared location: `/opt/robocorp` + +Note: On Windows the command below assumes the standard `BUILTIN\Users` +user group is present. +If your organization has replaced this you can grant the permission with: -```sh -sudo rcc holotree shared --enable +``` +icacls "C:\ProgramData\robocorp" /grant "BUILTIN\Users":(OI)(CI)M /T ``` -### Per user initialization - -If user wants to use shared holotrees, then they have to initialize to use -those shared settings. That can be done using following command. +To switch the user to using shared holotrees use the following command. ```sh rcc holotree init From f1d3eba567f60b7dc20b8224545bc22c15fcc578 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 10 Jun 2022 18:13:39 +0300 Subject: [PATCH 271/516] QUICKFIX: documentation TOC update. --- docs/README.md | 343 +------------------------------------------------ 1 file changed, 4 insertions(+), 339 deletions(-) diff --git a/docs/README.md b/docs/README.md index 7af6812b..7aed3543 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,342 +70,7 @@ #### 4.7.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) #### 4.7.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) #### 4.7.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#rcc----how-to-build-it) -### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#tooling) -### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#commands) -### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs\BUILD.md#where-to-start-reading-code) -## 6 [rcc change log](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#rcc-change-log) -### 6.1 [v11.14.3 (date: 9.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.2 [v11.14.2 (date: 8.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.3 [v11.14.1 (date: 8.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.4 [v11.14.0 (date: 7.6.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.5 [v11.13.0 (date: 19.5.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.6 [v11.12.9 (date: 17.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.7 [v11.12.8 (date: 16.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.8 [v11.12.7 (date: 12.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.9 [v11.12.6 (date: 10.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.10 [v11.12.5 (date: 9.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.11 [v11.12.4 (date: 9.5.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.12 [v11.12.3 (date: 5.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.13 [v11.12.2 (date: 5.5.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.14 [v11.12.1 (date: 29.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.15 [v11.12.0 (date: 28.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.16 [v11.11.2 (date: 26.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.17 [v11.11.1 (date: 26.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.18 [v11.11.0 (date: 25.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.19 [v11.10.7 (date: 22.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.20 [v11.10.6 (date: 21.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.21 [v11.10.5 (date: 20.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.22 [v11.10.4 (date: 20.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.23 [v11.10.3 (date: 19.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.24 [v11.10.2 (date: 13.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.25 [v11.10.1 (date: 12.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.26 [v11.10.0 (date: 12.4.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.27 [v11.9.16 (date: 7.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.28 [v11.9.15 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.29 [v11.9.14 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.30 [v11.9.13 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.31 [v11.9.12 (date: 6.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.32 [v11.9.11 (date: 5.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.33 [v11.9.10 (date: 4.4.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.34 [v11.9.9 (date: 31.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.35 [v11.9.8 (date: 29.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.36 [v11.9.7 (date: 28.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.37 [v11.9.6 (date: 25.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.38 [v11.9.5 (date: 23.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.39 [v11.9.4 (date: 22.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.40 [v11.9.3 (date: 22.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.41 [v11.9.2 (date: 18.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.42 [v11.9.1 (date: 10.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.43 [v11.9.0 (date: 9.3.2022) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.44 [v11.8.0 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.45 [v11.7.1 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.46 [v11.7.0 (date: 8.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.47 [v11.6.6 (date: 7.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.48 [v11.6.5 (date: 2.3.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.49 [v11.6.4 (date: 23.2.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.50 [v11.6.3 (date: 10.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.51 [v11.6.2 (date: 7.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.52 [v11.6.1 (date: 7.1.2022)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.53 [v11.6.0 (date: 7.12.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.54 [v11.5.5 (date: 2.11.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.55 [v11.5.4 (date: 29.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.56 [v11.5.3 (date: 28.10.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.57 [v11.5.2 (date: 27.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.58 [v11.5.1 (date: 26.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.59 [v11.5.0 (date: 20.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.60 [v11.4.3 (date: 20.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.61 [v11.4.2 (date: 19.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.62 [v11.4.1 (date: 18.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.63 [v11.4.0 (date: 18.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.64 [v11.3.6 (date: 13.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.65 [v11.3.5 (date: 12.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.66 [v11.3.4 (date: 12.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.67 [v11.3.3 (date: 8.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.68 [v11.3.2 (date: 7.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.69 [v11.3.1 (date: 5.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.70 [v11.3.0 (date: 4.10.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.71 [v11.2.0 (date: 29.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.72 [v11.1.6 (date: 27.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -#### 6.72.1 [What to consider when upgrading from series 10 to series 11 of rcc?](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#what-to-consider-when-upgrading-from-series-to-series-of-rcc) -### 6.73 [v11.1.5 (date: 24.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.74 [v11.1.4 (date: 23.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.75 [v11.1.3 (date: 21.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.76 [v11.1.2 (date: 20.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.77 [v11.1.1 (date: 17.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.78 [v11.1.0 (date: 16.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.79 [v11.0.8 (date: 15.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.80 [v11.0.7 (date: 14.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.81 [v11.0.6 (date: 13.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.82 [v11.0.5 (date: 10.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.83 [v11.0.4 (date: 9.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.84 [v11.0.3 (date: 8.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.85 [v11.0.2 (date: 8.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.86 [v11.0.1 (date: 7.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.87 [v11.0.0 (date: 6.9.2021) UNSTABLE](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---unstable) -### 6.88 [v10.10.0 (date: 7.9.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.89 [v10.9.4 (date: 31.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.90 [v10.9.3 (date: 31.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.91 [v10.9.2 (date: 30.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.92 [v10.9.1 (date: 27.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.93 [v10.9.0 (date: 25.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.94 [v10.8.1 (date: 24.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.95 [v10.8.0 (date: 19.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.96 [v10.7.1 (date: 18.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.97 [v10.7.0 (date: 16.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.98 [v10.6.0 (date: 16.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.99 [v10.5.2 (date: 12.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.100 [v10.5.1 (date: 11.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.101 [v10.5.0 (date: 10.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.102 [v10.4.5 (date: 10.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.103 [v10.4.4 (date: 9.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.104 [v10.4.3 (date: 9.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.105 [v10.4.2 (date: 5.8.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.106 [v10.4.1 (date: 5.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.107 [v10.4.0 (date: 5.8.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.108 [v10.3.3 (date: 29.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.109 [v10.3.2 (date: 29.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.110 [v10.3.1 (date: 29.6.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.111 [v10.3.0 (date: 28.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.112 [v10.2.4 (date: 24.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.113 [v10.2.3 (date: 24.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.114 [v10.2.2 (date: 23.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.115 [v10.2.1 (date: 21.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.116 [v10.2.0 (date: 21.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.117 [v10.1.1 (date: 18.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.118 [v10.1.0 (date: 17.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.119 [v10.0.0 (date: 15.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.120 [v9.20.0 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.121 [v9.19.4 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.122 [v9.19.3 (date: 10.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.123 [v9.19.2 (date: 9.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.124 [v9.19.1 (date: 8.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.125 [v9.19.0 (date: 8.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.126 [v9.18.0 (date: 3.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.127 [v9.17.2 (date: 2.6.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.128 [v9.17.1 (date: 2.6.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.129 [v9.17.0 (date: 26.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.130 [v9.16.0 (date: 21.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.131 [v9.15.1 (date: 21.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.132 [v9.15.0 (date: 20.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.133 [v9.14.0 (date: 19.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.134 [v9.13.0 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.135 [v9.12.1 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.136 [v9.12.0 (date: 18.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.137 [v9.11.3 (date: 12.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.138 [v9.11.2 (date: 11.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.139 [v9.11.1 (date: 7.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.140 [v9.11.0 (date: 6.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.141 [v9.10.2 (date: 5.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.142 [v9.10.1 (date: 5.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.143 [v9.10.0 (date: 4.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.144 [v9.9.21 (date: 4.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.145 [v9.9.20 (date: 3.5.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.146 [v9.9.19 (date: 29.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.147 [v9.9.18 (date: 28.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.148 [v9.9.17 (date: 20.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.149 [v9.9.16 (date: 20.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.150 [v9.9.15 (date: 19.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.151 [v9.9.14 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.152 [v9.9.13 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.153 [v9.9.12 (date: 15.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.154 [v9.9.11 (date: 13.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.155 [v9.9.10 (date: 12.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.156 [v9.9.9 (date: 9.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.157 [v9.9.8 (date: 9.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.158 [v9.9.7 (date: 8.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.159 [v9.9.6 (date: 8.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.160 [v9.9.5 (date: 6.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.161 [v9.9.4 (date: 6.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.162 [v9.9.3 (date: 1.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.163 [v9.9.2 (date: 1.4.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.164 [v9.9.1 (date: 31.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.165 [v9.9.0 (date: 31.3.2021) broken](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date---broken) -### 6.166 [v9.8.11 (date: 30.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.167 [v9.8.10 (date: 30.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.168 [v9.8.9 (date: 29.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.169 [v9.8.8 (date: 29.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.170 [v9.8.7 (date: 26.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.171 [v9.8.6 (date: 25.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.172 [v9.8.5 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.173 [v9.8.4 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.174 [v9.8.3 (date: 24.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.175 [v9.8.2 (date: 23.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.176 [v9.8.1 (date: 22.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.177 [v9.8.0 (date: 18.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.178 [v9.7.4 (date: 17.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.179 [v9.7.3 (date: 16.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.180 [v9.7.2 (date: 11.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.181 [v9.7.1 (date: 10.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.182 [v9.7.0 (date: 10.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.183 [v9.6.2 (date: 5.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.184 [v9.6.1 (date: 3.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.185 [v9.6.0 (date: 3.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.186 [v9.5.4 (date: 2.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.187 [v9.5.3 (date: 2.3.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.188 [v9.5.2 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.189 [v9.5.1 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.190 [v9.5.0 (date: 25.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.191 [v9.4.4 (date: 24.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.192 [v9.4.3 (date: 23.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.193 [v9.4.2 (date: 23.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.194 [v9.4.1 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.195 [v9.4.0 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.196 [v9.3.12 (date: 17.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.197 [v9.3.11 (date: 15.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.198 [v9.3.10 (date: 11.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.199 [v9.3.9 (date: 8.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.200 [v9.3.8 (date: 4.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.201 [v9.3.7 (date: 4.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.202 [v9.3.6 (date: 3.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.203 [v9.3.5 (date: 2.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.204 [v9.3.4 (date: 1.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.205 [v9.3.3 (date: 1.2.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.206 [v9.3.2 (date: 29.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.207 [v9.3.1 (date: 29.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.208 [v9.3.0 (date: 28.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.209 [v9.2.0 (date: 25.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.210 [v9.1.0 (date: 25.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.211 [v9.0.2 (date: 21.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.212 [v9.0.1 (date: 20.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.213 [v9.0.0 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.214 [v8.0.12 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.215 [v8.0.10 (date: 18.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.216 [v8.0.9 (date: 15.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.217 [v8.0.8 (date: 15.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.218 [v8.0.7 (date: 11.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.219 [v8.0.6 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.220 [v8.0.5 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.221 [v8.0.4 (date: 8.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.222 [v8.0.3 (date: 7.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.223 [v8.0.2 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.224 [v8.0.1 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.225 [v8.0.0 (date: 5.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.226 [v7.1.5 (date: 4.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.227 [v7.1.4 (date: 4.1.2021)](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#v---date--) -### 6.228 [Older versions](https://github.com/robocorp/rcc/blob/master/docs\changelog.md#older-versions) -### 6.229 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#what-is-execution-environment-isolation-and-caching) -### 6.230 [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#the-second-evolution-of-environment-management-in-rcc) -#### 6.230.1 [Relocation and file locking](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#relocation-and-file-locking) -### 6.231 [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#a-better-analogy-accommodations) -#### 6.231.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#i-invited-you-to-my-home) -#### 6.231.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) -#### 6.231.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs\environment-caching.md#welcome-to-an-actual-hotel) -## 7 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs\features.md#incomplete-list-of-rcc-features) -## 8 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#profile-configuration) -### 8.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-is-profile) -#### 8.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#when-do-you-need-profiles) -#### 8.1.2 [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-does-it-contain) -### 8.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#quick-start-guide) -### 8.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#what-is-needed) -### 8.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs\profile_configuration.md#discovery-process) -## 9 [Table of contents: rcc documentation](https://github.com/robocorp/rcc/blob/master/docs\README.md#table-of-contents-rcc-documentation) -### 9.1 [1 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#incomplete-list-of-rcc-use-cases)](https://github.com/robocorp/rcc/blob/master/docs\README.md#incomplete-list-of-rcc-use-cases-https-githubcom-robocorp-rcc-blob-master-docs-usecasesmd-incomplete-list-of-rcc-use-cases) -#### 9.1.1 [1.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs/usecases.md#what-is-available-from-conda-forge)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-available-from-conda-forge-https-githubcom-robocorp-rcc-blob-master-docs-usecasesmd-what-is-available-from-conda-forge) -### 9.2 [2 [Incomplete list of rcc features](https://github.com/robocorp/rcc/blob/master/docs/features.md#incomplete-list-of-rcc-features)](https://github.com/robocorp/rcc/blob/master/docs\README.md#incomplete-list-of-rcc-features-https-githubcom-robocorp-rcc-blob-master-docs-featuresmd-incomplete-list-of-rcc-features) -### 9.3 [3 [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#tips-tricks-and-recipies)](https://github.com/robocorp/rcc/blob/master/docs\README.md#tips-tricks-and-recipies-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-tips-tricks-and-recipies) -#### 9.3.1 [3.1 [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-see-dependency-changes)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-see-dependency-changes-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-see-dependency-changes) -#### 9.3.2 [3.2 [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-freeze-dependencies)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-freeze-dependencies-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-freeze-dependencies) -#### 9.3.3 [3.3 [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-pass-arguments-to-robot-from-cli)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-pass-arguments-to-robot-from-cli-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-pass-arguments-to-robot-from-cli) -#### 9.3.4 [3.4 [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-run-any-command-inside-robot-environment)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-run-any-command-inside-robot-environment-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-run-any-command-inside-robot-environment) -#### 9.3.5 [3.5 [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-convert-existing-python-project-to-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-convert-existing-python-project-to-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-convert-existing-python-project-to-rcc) -#### 9.3.6 [3.6 [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#is-rcc-limited-to-python-and-robot-framework)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-is-rcc-limited-to-python-and-robot-framework-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-is-rcc-limited-to-python-and-robot-framework) -#### 9.3.7 [3.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#think-what-you-can-do-with-this-condayaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-think-what-you-can-do-with-this-condayaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-think-what-you-can-do-with-this-condayaml) -#### 9.3.8 [3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-control-holotree-environments-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-control-holotree-environments) -#### 9.3.9 [3.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-shared-holotree-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-shared-holotree) -#### 9.3.10 [3.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-setup-rcc-to-use-shared-holotree-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-setup-rcc-to-use-shared-holotree) -#### 9.3.11 [3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-can-be-controlled-using-environment-variables-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-can-be-controlled-using-environment-variables) -#### 9.3.12 [3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-how-to-troubleshoot-rcc-setup-and-robots-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-how-to-troubleshoot-rcc-setup-and-robots) -#### 9.3.13 [3.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-in-robotyaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-in-robotyaml) -#### 9.3.14 [3.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-in-condayaml-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-is-in-condayaml) -#### 9.3.15 [3.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-where-can-i-find-updates-for-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-where-can-i-find-updates-for-rcc) -#### 9.3.16 [3.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-has-changed-on-rcc-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-what-has-changed-on-rcc) -#### 9.3.17 [3.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-can-i-see-these-tips-as-web-page-https-githubcom-robocorp-rcc-blob-master-docs-recipesmd-can-i-see-these-tips-as-web-page) -### 9.4 [4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration)](https://github.com/robocorp/rcc/blob/master/docs\README.md#profile-configuration-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-profile-configuration) -#### 9.4.1 [4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-profile-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-what-is-profile) -#### 9.4.2 [4.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-quick-start-guide-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-quick-start-guide) -#### 9.4.3 [4.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-needed-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-what-is-needed) -#### 9.4.4 [4.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-discovery-process-https-githubcom-robocorp-rcc-blob-master-docs-profile-configurationmd-discovery-process) -#### 9.4.5 [4.5 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-what-is-execution-environment-isolation-and-caching-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-what-is-execution-environment-isolation-and-caching) -#### 9.4.6 [4.6 [The second evolution of environment management in RCC](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#the-second-evolution-of-environment-management-in-rcc)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-the-second-evolution-of-environment-management-in-rcc-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-the-second-evolution-of-environment-management-in-rcc) -#### 9.4.7 [4.7 [A better analogy: accommodations](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#a-better-analogy-accommodations)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-a-better-analogy-accommodations-https-githubcom-robocorp-rcc-blob-master-docs-environment-cachingmd-a-better-analogy-accommodations) -### 9.5 [5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it)](https://github.com/robocorp/rcc/blob/master/docs\README.md#rcc----how-to-build-it-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-rcc----how-to-build-it) -#### 9.5.1 [5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-tooling-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-tooling) -#### 9.5.2 [5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-commands-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-commands) -#### 9.5.3 [5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code)](https://github.com/robocorp/rcc/blob/master/docs\README.md#-where-to-start-reading-code-https-githubcom-robocorp-rcc-blob-master-docs-buildmd-where-to-start-reading-code) -## 10 [Tips, tricks, and recipies](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#tips-tricks-and-recipies) -### 10.1 [How to see dependency changes?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-see-dependency-changes) -#### 10.1.1 [Why is this important?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#why-is-this-important) -#### 10.1.2 [Example of dependencies listing from holotree environment](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example-of-dependencies-listing-from-holotree-environment) -### 10.2 [How to freeze dependencies?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-freeze-dependencies) -#### 10.2.1 [Steps](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#steps) -#### 10.2.2 [Limitations](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#limitations) -### 10.3 [How pass arguments to robot from CLI?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-pass-arguments-to-robot-from-cli) -#### 10.3.1 [Example robot.yaml with scripting task](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example-robotyaml-with-scripting-task) -#### 10.3.2 [Run it with `--` separator.](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#run-it-with----separator) -### 10.4 [How to run any command inside robot environment?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-run-any-command-inside-robot-environment) -#### 10.4.1 [Some example commands](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#some-example-commands) -### 10.5 [How to convert existing python project to rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-convert-existing-python-project-to-rcc) -#### 10.5.1 [Basic workflow to get it up and running](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#basic-workflow-to-get-it-up-and-running) -#### 10.5.2 [What next?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-next) -### 10.6 [Is rcc limited to Python and Robot Framework?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#is-rcc-limited-to-python-and-robot-framework) -#### 10.6.1 [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#this-is-what-we-are-going-to-do-) -#### 10.6.2 [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-robotyaml) -#### 10.6.3 [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-condayaml) -#### 10.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#write-a-bin-buildersh) -### 10.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#think-what-you-can-do-with-this-condayaml) -### 10.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-control-holotree-environments) -#### 10.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-get-understanding-on-holotree) -#### 10.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-activate-holotree-environment) -### 10.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-shared-holotree) -### 10.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-setup-rcc-to-use-shared-holotree) -#### 10.10.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#one-time-setup) -#### 10.10.2 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#reverting-back-to-private-holotrees) -### 10.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-can-be-controlled-using-environment-variables) -### 10.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### 10.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#additional-debugging-options) -### 10.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-in-robotyaml) -#### 10.13.1 [Example](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example) -#### 10.13.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-this-robotyaml-thing) -#### 10.13.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-tasks) -#### 10.13.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-devtasks) -#### 10.13.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-condaconfigfile) -#### 10.13.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-environmentconfigs) -#### 10.13.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-prerunscripts) -#### 10.13.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-artifactsdir) -#### 10.13.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-ignorefiles) -#### 10.13.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-path) -#### 10.13.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-pythonpath) -### 10.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-in-condayaml) -#### 10.14.1 [Example](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#example) -#### 10.14.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-is-this-condayaml-thing) -#### 10.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-channels) -#### 10.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-dependencies) -#### 10.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-are-rccpostinstall-scripts) -### 10.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#where-can-i-find-updates-for-rcc) -### 10.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#what-has-changed-on-rcc) -#### 10.16.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#see-changelog-from-git-repo-) -#### 10.16.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#see-that-from-your-version-of-rcc-directly-) -### 10.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs\recipes.md#can-i-see-these-tips-as-web-page) -## 11 [Incomplete list of rcc use cases](https://github.com/robocorp/rcc/blob/master/docs\usecases.md#incomplete-list-of-rcc-use-cases) -### 11.1 [What is available from conda-forge?](https://github.com/robocorp/rcc/blob/master/docs\usecases.md#what-is-available-from-conda-forge) \ No newline at end of file +## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it) +### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) +### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) +### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file From 719314511a622068f7fcb8f9946625fe0a37fa7d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 15 Jun 2022 11:36:11 +0300 Subject: [PATCH 272/516] FEATURE: using icacls in Windows (v11.14.4) - holotree share enabling now uses "icals" in Windows to set default properties - added marker file "shared.yes" when shared has been executed --- cmd/command_darwin.go | 15 +++++++++++++-- cmd/command_linux.go | 6 ++++++ cmd/command_windows.go | 23 +++++++++++++++++++++-- common/variables.go | 4 ++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cmd/command_darwin.go b/cmd/command_darwin.go index 49852d96..fa53a5d3 100644 --- a/cmd/command_darwin.go +++ b/cmd/command_darwin.go @@ -1,7 +1,18 @@ package cmd -import "github.com/robocorp/rcc/pretty" +import ( + "os" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" +) func osSpecificHolotreeSharing(enable bool) { - pretty.Warning("Good to go. Nothing to do on Mac OS.") + if !enable { + return + } + pathlib.ForceShared() + err := os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) + pretty.Guard(err == nil, 3, "Could not write %q, reason: %v", common.SharedMarkerLocation(), err) } diff --git a/cmd/command_linux.go b/cmd/command_linux.go index 88bc11a3..5bbaa39e 100644 --- a/cmd/command_linux.go +++ b/cmd/command_linux.go @@ -1,6 +1,7 @@ package cmd import ( + "os" "path/filepath" "github.com/robocorp/rcc/common" @@ -9,10 +10,15 @@ import ( ) func osSpecificHolotreeSharing(enable bool) { + if !enable { + return + } pathlib.ForceShared() parent := filepath.Dir(common.HoloLocation()) _, err := pathlib.ForceSharedDir(parent) pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) _, err = pathlib.ForceSharedDir(common.HoloLocation()) pretty.Guard(err == nil, 2, "Could not enable shared location at %q, reason: %v", common.HoloLocation(), err) + err = os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) + pretty.Guard(err == nil, 3, "Could not write %q, reason: %v", common.SharedMarkerLocation(), err) } diff --git a/cmd/command_windows.go b/cmd/command_windows.go index 0efc44f9..6a378ee5 100644 --- a/cmd/command_windows.go +++ b/cmd/command_windows.go @@ -1,7 +1,26 @@ package cmd -import "github.com/robocorp/rcc/pretty" +import ( + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/shell" +) func osSpecificHolotreeSharing(enable bool) { - pretty.Warning("Good to go. Nothing to do on Windows.") + if !enable { + return + } + pathlib.ForceShared() + parent := filepath.Dir(common.HoloLocation()) + _, err := pathlib.ForceSharedDir(parent) + pretty.Guard(err == nil, 1, "Could not enable shared location at %q, reason: %v", parent, err) + task := shell.New(nil, ".", "icacls", "C:/ProgramData/robocorp", "/grant", "BUILTIN\\Users:(OI)(CI)M", "/T", "/Q") + _, err = task.Execute(false) + pretty.Guard(err == nil, 2, "Could not set 'icacls' settings, reason: %v", err) + err = os.WriteFile(common.SharedMarkerLocation(), []byte(common.Version), 0644) + pretty.Guard(err == nil, 3, "Could not write %q, reason: %v", common.SharedMarkerLocation(), err) } diff --git a/common/variables.go b/common/variables.go index cfab4c38..7b2ae1b0 100644 --- a/common/variables.go +++ b/common/variables.go @@ -123,6 +123,10 @@ func BinLocation() string { return filepath.Join(RobocorpHome(), "bin") } +func SharedMarkerLocation() string { + return filepath.Join(HoloLocation(), "shared.yes") +} + func HoloLocation() string { return ExpandPath(defaultHoloLocation) } diff --git a/common/version.go b/common/version.go index 13916820..7f34ee2a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.3` + Version = `v11.14.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 952abee0..f6d09ecb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.14.4 (date: 15.6.2022) + +- holotree share enabling now uses "icals" in Windows to set default properties +- added marker file "shared.yes" when shared has been executed + ## v11.14.3 (date: 9.6.2022) - upgraded rcc to be build using go v1.18.x From 2d59b8af5ddd7a87319f1b397a58afa18d52cc2a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 22 Jun 2022 10:56:01 +0300 Subject: [PATCH 273/516] FIX: shared holotree enabling once (v11.14.5) - added `--once` flag to holotree shared enabling, in cases where costly sharing is required only once --- cmd/holotreeShared.go | 9 +++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cmd/holotreeShared.go b/cmd/holotreeShared.go index ecfa92da..d1774be5 100644 --- a/cmd/holotreeShared.go +++ b/cmd/holotreeShared.go @@ -4,6 +4,7 @@ import ( "os" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -11,6 +12,7 @@ import ( var ( enableShared bool + onlyOnce bool ) var holotreeSharedCommand = &cobra.Command{ @@ -21,6 +23,12 @@ var holotreeSharedCommand = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Enabling shared holotree lasted").Report() } + enabled := pathlib.IsFile(common.SharedMarkerLocation()) + if enabled && onlyOnce { + pretty.Warning("Seems that sharing is already enabled! Quitting! [--once]") + pretty.Ok() + return + } if os.Geteuid() > 0 { pretty.Warning("Running this command might need sudo/root access rights. Still, trying ...") } @@ -31,5 +39,6 @@ var holotreeSharedCommand = &cobra.Command{ func init() { holotreeSharedCommand.Flags().BoolVarP(&enableShared, "enable", "e", false, "Enable shared holotree environments between users. Currently cannot be undone.") + holotreeSharedCommand.Flags().BoolVarP(&onlyOnce, "once", "o", false, "Only try enabling if it has not been done yet.") holotreeCmd.AddCommand(holotreeSharedCommand) } diff --git a/common/version.go b/common/version.go index 7f34ee2a..63b702b0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.4` + Version = `v11.14.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index f6d09ecb..f0224d4e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.14.5 (date: 22.6.2022) + +- added `--once` flag to holotree shared enabling, in cases where costly + sharing is required only once + ## v11.14.4 (date: 15.6.2022) - holotree share enabling now uses "icals" in Windows to set default properties From 6dcce9292de8b8ca006d4e3743155b384ad1edb0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 7 Jul 2022 15:25:54 +0300 Subject: [PATCH 274/516] UPGRADE: micromamba to version 0.24.0 (v11.15.0) - micromamba upgrade to v0.24.0 --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 4 ++++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 63b702b0..2c5daa97 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.14.5` + Version = `v11.15.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 3580cf29..c750eb04 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.2/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.24.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 310cb3bf..0646d1f2 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.2/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.24.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index 72bec631..fabece23 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.23.2/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.24.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 50090e72..7ce79280 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -197,7 +197,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 23002 + goodEnough := version >= 24000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index f0224d4e..268ff942 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.15.0 (date: 7.7.2022) + +- micromamba upgrade to v0.24.0 + ## v11.14.5 (date: 22.6.2022) - added `--once` flag to holotree shared enabling, in cases where costly From da87ef9f20c0ca10dcc0ccc2ef511abb8de717bf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 8 Jul 2022 12:58:10 +0300 Subject: [PATCH 275/516] DOC: old school CI recipe (v11.15.1) --- common/version.go | 2 +- docs/README.md | 15 ++++++---- docs/changelog.md | 4 +++ docs/recipes.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 2c5daa97..6ac65a80 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.15.0` + Version = `v11.15.1` ) diff --git a/docs/README.md b/docs/README.md index 7aed3543..ac07f53b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,11 +51,16 @@ #### 3.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) #### 3.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) #### 3.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.15 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.16 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.16.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.16.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.17 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +### 3.15 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-ci-cd-pipeline-integration-with-rcc) +#### 3.15.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) +#### 3.15.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) +#### 3.15.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-ci-cd-step-in-local-machine) +#### 3.15.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) +### 3.16 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.17 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.17.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.17.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.18 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) diff --git a/docs/changelog.md b/docs/changelog.md index 268ff942..9191ea76 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.15.1 (date: 8.7.2022) + +- added "old school CI" recipe into documentation + ## v11.15.0 (date: 7.7.2022) - micromamba upgrade to v0.24.0 diff --git a/docs/recipes.md b/docs/recipes.md index 70846879..8afa47da 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -700,6 +700,77 @@ who has access to that cache. If you need to have private or sensitive packages in your environment, see `preRunScripts` in `robot.yaml` file. +## How to do "old-school" CI/CD pipeline integration with rcc? + +If you have CI/CD pipeline and want to updated your robots from there, this +recipe should give you ideas how to do it. This example works in linux, and +you probably have to modify it to work on Mac or Windows, but idea will be same. + +Basic requirements are: +- have well formed robot in version control +- have rcc command available or possibility to fetch it +- possibility on CI/CD pipeline to run just simple CLI commands + +### The oldschoolci.sh script + +```sh +#!/bin/sh -ex + +curl -o rcc https://downloads.robocorp.com/rcc/releases/v11.14.3/linux64/rcc +chmod 755 rcc +./rcc cloud push --account ${ACCOUNT_ID} --directory ${ROBOT_DIR} --workspace ${WORKSPACE_ID} --robot ${ROBOT_ID} +``` + +So above script uses `curl` command to download rcc from download site, and +makes it executable. And then it simply calls that `rcc` command, and expects +that CI system has provided few variables. + +### A setup.sh script for simulating variable injection. + +```sh +#!/bin/sh + +export ACCOUNT_ID=4242:cafe9d9c0dadag00d37b9577babe1575b67bc1bbad3ce9484dead36a649c865beef26297e67c8d94f0f0057f0100ab64:https://api.eu1.robocorp.com +export WORKSPACE_ID=1717 +export ROBOT_ID=2121 +export ROBOT_DIR=$(pwd)/therobot +``` + +Expectations for above setup are: +- robot to be updated is in EU1 (behind https://api.eu1.robocorp.com API) +- Control Room account has "Access creadentials" 4242 available and active +- account has access to workspace 1717 +- there exist previously created robot 2121 in that workspace +- robot is located in "therobot" directory directly under "current working + directory" (centered around `robot.yaml` file) +- and account has suitable rights to actually push robot to Control Room + +### Simulating actual CI/CD step in local machine. + +```sh +#!/bin/sh -ex + +source setup.sh +./oldschoolci.sh +``` + +Above script brings "setup" and "old school CI" together, but just for +demonstration purposes. For real life use, adapt and remember security (no +compromising variable content inside repository). + +### Additional notes + +- if CI/CD worker/container can be custom build, then it is recommended to + download rcc just once and not on every run (like oldschoolci.sh script now + does) +- that `ACCOUNT_ID` should be stored in credentials store/vault in CI system, + because that is secret that you need to use to be able to push to cloud +- that `ACCOUNT_ID` is "ephemeral" account, and will not be saved in `rcc.yaml` +- also consider saving other variables in secure way +- in actual CI/CD pipeline, you might want to embed actual commands into + CI step recipe and not have external scripts (but you decide that) + + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html From 094648235626faa85596017d63e65ae710dfde1b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 11 Jul 2022 09:49:29 +0300 Subject: [PATCH 276/516] FIX: scripts/toc.py fix (v11.15.2) - fixed table of contents links to match Github generated ones - also tried to make toc.py more OS neutral (was failing on Windows) --- common/version.go | 2 +- docs/README.md | 6 +++--- docs/changelog.md | 5 +++++ scripts/toc.py | 23 +++++++++++------------ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/common/version.go b/common/version.go index 6ac65a80..94b8d4c9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.15.1` + Version = `v11.15.2` ) diff --git a/docs/README.md b/docs/README.md index ac07f53b..cf0152a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ #### 3.6.1 [This is what we are going to do ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#this-is-what-we-are-going-to-do-) #### 3.6.2 [Write a robot.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-robotyaml) #### 3.6.3 [Write a conda.yaml](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-condayaml) -#### 3.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-bin-buildersh) +#### 3.6.4 [Write a bin/builder.sh](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#write-a-binbuildersh) ### 3.7 [Think what you can do with this conda.yaml?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#think-what-you-can-do-with-this-condayaml) ### 3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) #### 3.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) @@ -51,10 +51,10 @@ #### 3.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) #### 3.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) #### 3.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.15 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-ci-cd-pipeline-integration-with-rcc) +### 3.15 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-cicd-pipeline-integration-with-rcc) #### 3.15.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) #### 3.15.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) -#### 3.15.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-ci-cd-step-in-local-machine) +#### 3.15.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) #### 3.15.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) ### 3.16 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) ### 3.17 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) diff --git a/docs/changelog.md b/docs/changelog.md index 9191ea76..4d633105 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.15.2 (date: 11.7.2022) + +- fixed table of contents links to match Github generated ones +- also tried to make toc.py more OS neutral (was failing on Windows) + ## v11.15.1 (date: 8.7.2022) - added "old school CI" recipe into documentation diff --git a/scripts/toc.py b/scripts/toc.py index 198d7cd6..255c058f 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -2,7 +2,9 @@ import glob import re +from os.path import basename +DELETE_PATTERN = re.compile(r'[/:]+') NONCHAR_PATTERN = re.compile(r'[^.a-z-]+') HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') CODE_PATTERN = re.compile(r'^\s*[`]{3}') @@ -11,11 +13,7 @@ DASH = '-' NEWLINE = '\n' -IGNORE_LIST = ( - 'docs/changelog.md', - 'docs/toc.md', - 'docs/README.md', - ) +IGNORE_LIST = ('changelog.md', 'toc.md', 'README.md') PRIORITY_LIST = ( 'docs/usecases.md', @@ -26,7 +24,7 @@ ) def unify(value): - low = str(value).lower() + low = DELETE_PATTERN.sub('', str(value).lower()) return DASH.join(filter(bool, NONCHAR_PATTERN.split(low))).replace('.', '') class Toc: @@ -72,16 +70,17 @@ def headings(filename): def process(): toc = Toc("Table of contents: rcc documentation", "https://github.com/robocorp/rcc/blob/master/") - documentation = list(glob.glob('docs/*.md')) + flatnames = list(map(basename, glob.glob('docs/*.md'))) for filename in PRIORITY_LIST: - if filename in documentation: - documentation.remove(filename) + flatname = basename(filename) + if flatname in flatnames: + flatnames.remove(flatname) for filename, level, title in headings(filename): toc.add(filename, level, title) - for filename in documentation: - if filename in IGNORE_LIST: + for flatname in flatnames: + if flatname in IGNORE_LIST: continue - for filename, level, title in headings(filename): + for filename, level, title in headings(f'docs/{flatname}'): toc.add(filename, level, title) toc.write('docs/README.md') From 32c5eaf9cd7610fd6dc79dc55c229a41de5674d8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 13 Jul 2022 13:05:30 +0300 Subject: [PATCH 277/516] REFACTORING: module dependencies (v11.15.3) - refactoring module dependencies to help reusing parts of rcc in other apps --- cmd/speed.go | 4 ++-- common/algorithms.go | 6 ++++++ common/scorecard.go | 8 +++----- common/variables.go | 4 +--- common/version.go | 2 +- conda/workflows.go | 3 +-- docs/changelog.md | 4 ++++ go.mod | 8 +++----- go.sum | 7 +------ htfs/library.go | 3 +-- operations/diagnostics.go | 2 +- operations/initialize.go | 2 +- operations/running.go | 3 +-- robot/robot.go | 4 ++-- settings/data.go | 2 +- settings/profile.go | 2 +- shell/task.go | 11 +++++++++++ 17 files changed, 41 insertions(+), 34 deletions(-) diff --git a/cmd/speed.go b/cmd/speed.go index 6862bce1..6e39339a 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -87,9 +87,9 @@ var speedtestCmd = &cobra.Command{ close(signal) if !debug { elapsed := <-timing - common.Log("%s", score.Score(elapsed)) + common.Log("%s", score.Score(anywork.Scale(), elapsed)) } else { - common.Log("%s", score.Score(0.0)) + common.Log("%s", score.Score(anywork.Scale(), 0.0)) } pretty.Ok() }, diff --git a/common/algorithms.go b/common/algorithms.go index 0ed5bed2..316a6dca 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -4,6 +4,8 @@ import ( "crypto/sha256" "fmt" "math" + + "github.com/dchest/siphash" ) func Entropy(input []byte) float64 { @@ -37,3 +39,7 @@ func ShortDigest(content string) string { result := Hexdigest(digester.Sum(nil)) return result[:16] } + +func Siphash(left, right uint64, body []byte) uint64 { + return siphash.Hash(left, right, body) +} diff --git a/common/scorecard.go b/common/scorecard.go index 37fb9175..17acb337 100644 --- a/common/scorecard.go +++ b/common/scorecard.go @@ -3,8 +3,6 @@ package common import ( "fmt" "time" - - "github.com/robocorp/rcc/anywork" ) const ( @@ -23,7 +21,7 @@ type Scorecard interface { Start() Scorecard Midpoint() Scorecard Done() Scorecard - Score(int) string + Score(uint64, int) string } type scorecard struct { @@ -32,7 +30,7 @@ type scorecard struct { filesystem time.Time } -func (it *scorecard) Score(seconds int) string { +func (it *scorecard) Score(scale uint64, seconds int) string { network := it.network.Sub(it.start).Milliseconds() filesystem := it.filesystem.Sub(it.network).Milliseconds() Debug("Raw score values: network=%d and filesystem=%d", network, filesystem) @@ -40,7 +38,7 @@ func (it *scorecard) Score(seconds int) string { return "Score: N/A [measurement not done]" } - return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale), seconds, anywork.Scale(), Platform()) + return fmt.Sprintf(perfMessage, topScale-(network/netScale), topScale-(filesystem/fsScale), seconds, scale, Platform()) } func (it *scorecard) Start() Scorecard { diff --git a/common/variables.go b/common/variables.go index 7b2ae1b0..6ca274c4 100644 --- a/common/variables.go +++ b/common/variables.go @@ -8,8 +8,6 @@ import ( "runtime" "strings" "time" - - "github.com/dchest/siphash" ) const ( @@ -264,6 +262,6 @@ func UserHomeIdentity() string { if err != nil { return "badcafe" } - digest := fmt.Sprintf("%02x", siphash.Hash(9007799254740993, 2147487647, []byte(location))) + digest := fmt.Sprintf("%02x", Siphash(9007799254740993, 2147487647, []byte(location))) return digest[:7] } diff --git a/common/version.go b/common/version.go index 94b8d4c9..3f19f3a4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.15.2` + Version = `v11.15.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index 9b2a6f08..dd3e4780 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/google/shlex" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -186,7 +185,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Progress(7, "Post install scripts phase started.") common.Debug("=== post install phase ===") for _, script := range postInstall { - scriptCommand, err := shlex.Split(script) + scriptCommand, err := shell.Split(script) if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) diff --git a/docs/changelog.md b/docs/changelog.md index 4d633105..e84a3330 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.15.3 (date: 13.7.2022) + +- refactoring module dependencies to help reusing parts of rcc in other apps + ## v11.15.2 (date: 11.7.2022) - fixed table of contents links to match Github generated ones diff --git a/go.mod b/go.mod index 33014c7e..97512847 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/robocorp/rcc go 1.14 require ( - github.com/dchest/siphash v1.2.2 // indirect + github.com/dchest/siphash v1.2.2 github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.12 github.com/mitchellh/mapstructure v1.2.2 // indirect @@ -17,9 +16,8 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.1 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b gopkg.in/ini.v1 v1.55.0 // indirect - gopkg.in/square/go-jose.v2 v2.5.1 // indirect - gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 + gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index b887f4e2..5c629985 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= -github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -67,6 +65,7 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -271,8 +270,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -335,8 +332,6 @@ gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/htfs/library.go b/htfs/library.go index ceed9536..cf56e330 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -12,7 +12,6 @@ import ( "sync" "time" - "github.com/dchest/siphash" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" @@ -340,7 +339,7 @@ func BlueprintHash(blueprint []byte) string { } func sipit(key []byte) uint64 { - return siphash.Hash(9007199254740993, 2147483647, key) + return common.Siphash(9007199254740993, 2147483647, key) } func textual(key uint64, size int) string { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 7075683e..32122499 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -22,7 +22,7 @@ import ( "github.com/robocorp/rcc/robot" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" - "gopkg.in/yaml.v1" + "gopkg.in/yaml.v2" ) const ( diff --git a/operations/initialize.go b/operations/initialize.go index f60df646..027e8d8b 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -16,7 +16,7 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" - "gopkg.in/yaml.v1" + "gopkg.in/yaml.v2" ) type StringMap map[string]string diff --git a/operations/running.go b/operations/running.go index ba6a9ff7..0f5014a4 100644 --- a/operations/running.go +++ b/operations/running.go @@ -6,7 +6,6 @@ import ( "runtime" "strings" - "github.com/google/shlex" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" @@ -258,7 +257,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if !robot.PlatformAcceptableFile(runtime.GOARCH, runtime.GOOS, script) { continue } - scriptCommand, err := shlex.Split(script) + scriptCommand, err := shell.Split(script) if err != nil { pretty.Exit(11, "%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) } diff --git a/robot/robot.go b/robot/robot.go index 2dc5055c..3005abc7 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -15,8 +15,8 @@ import ( "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/shell" - "github.com/google/shlex" "gopkg.in/yaml.v2" ) @@ -452,7 +452,7 @@ func (it *robot) RobotExecutionEnvironment(location string, inject []string, ful } func (it *task) shellCommand() []string { - result, err := shlex.Split(it.Shell) + result, err := shell.Split(it.Shell) if err != nil { common.Log("Shell parsing failure: %v with command %v", err, it.Shell) return []string{} diff --git a/settings/data.go b/settings/data.go index 2dc676e6..46e45e86 100644 --- a/settings/data.go +++ b/settings/data.go @@ -8,7 +8,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" - "gopkg.in/yaml.v1" + "gopkg.in/yaml.v2" ) const ( diff --git a/settings/profile.go b/settings/profile.go index 8923eb41..5a5db138 100644 --- a/settings/profile.go +++ b/settings/profile.go @@ -9,7 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" - "gopkg.in/yaml.v1" + "gopkg.in/yaml.v2" ) type Profile struct { diff --git a/shell/task.go b/shell/task.go index a975f413..f50092b3 100644 --- a/shell/task.go +++ b/shell/task.go @@ -7,9 +7,16 @@ import ( "os/exec" "path/filepath" + "github.com/google/shlex" "github.com/robocorp/rcc/common" ) +type Common interface { + Debug(string, ...interface{}) error + Trace(string, ...interface{}) error + Timeline(string, ...interface{}) +} + type Task struct { environment []string directory string @@ -18,6 +25,10 @@ type Task struct { stderronly bool } +func Split(commandline string) ([]string, error) { + return shlex.Split(commandline) +} + func New(environment []string, directory string, task ...string) *Task { executable, args := task[0], task[1:] return &Task{ From 6bd6bf60e93f09f4e7b1a937418a527187411412 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 13 Jul 2022 13:12:23 +0300 Subject: [PATCH 278/516] FIX: go-bindata accidentally remove (v11.15.4) --- common/version.go | 2 +- docs/changelog.md | 6 +++++- go.mod | 1 + go.sum | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 3f19f3a4..2271ba1c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.15.3` + Version = `v11.15.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index e84a3330..45ebc077 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v11.15.3 (date: 13.7.2022) +## v11.15.4 (date: 13.7.2022) + +- go-bindata was accidentally removed, adding it back + +## v11.15.3 (date: 13.7.2022) BROKEN - refactoring module dependencies to help reusing parts of rcc in other apps diff --git a/go.mod b/go.mod index 97512847..8b763861 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/dchest/siphash v1.2.2 github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.12 github.com/mitchellh/mapstructure v1.2.2 // indirect diff --git a/go.sum b/go.sum index 5c629985..03f2c30f 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= From 6049a522297843a6b66fd7b1493e11f61181e824 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 16 Aug 2022 11:19:16 +0300 Subject: [PATCH 279/516] UPGRADE: template and micromamba upgrade (v11.16.0) - micromamba upgrade to v0.25.1 - template upgrade of python to 3.9.13 - template upgrade of pip to 22.1.2 - template upgrade of rpaframework to 15.6.0 - upgraded tests to match above version changes and their effects --- assets/speedtest.yaml | 4 ++-- common/version.go | 2 +- conda/condayaml_test.go | 6 +++--- conda/config_test.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- conda/testdata/conda.yaml | 6 +++--- conda/testdata/other.yaml | 2 +- conda/testdata/third.yaml | 4 ++-- docs/changelog.md | 8 ++++++++ docs/recipes.md | 10 +++++----- robot_tests/bug_reports.robot | 6 +++--- robot_tests/export_holozip.robot | 10 +++++----- robot_tests/fullrun.robot | 10 +++++----- robot_tests/spellbug/conda.yaml | 4 ++-- robot_tests/templates.robot | 6 +++--- templates/extended/conda.yaml | 6 +++--- templates/python/conda.yaml | 6 +++--- templates/standard/conda.yaml | 6 +++--- 21 files changed, 57 insertions(+), 49 deletions(-) diff --git a/assets/speedtest.yaml b/assets/speedtest.yaml index 397d4624..099faea0 100644 --- a/assets/speedtest.yaml +++ b/assets/speedtest.yaml @@ -1,7 +1,7 @@ channels: - conda-forge dependencies: -- python=3.7.5 -- pip=20.1 +- python=3.9.13 +- pip=22.1.2 - pip: - robotframework==4.1.2 diff --git a/common/version.go b/common/version.go index 2271ba1c..1aacb095 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.15.4` + Version = `v11.16.0` ) diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index c944680e..a5636847 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -15,7 +15,7 @@ func TestCanParseDependencies(t *testing.T) { must_be.Equal("python", conda.AsDependency("python").Name) must_be.Equal("", conda.AsDependency("python").Qualifier) must_be.Equal("", conda.AsDependency("python").Versions) - wont_be.Nil(conda.AsDependency("python=3.7.5")) + wont_be.Nil(conda.AsDependency("python=3.9.13")) must_be.Equal("python=3.7.4", conda.AsDependency("python=3.7.4").Original) must_be.Equal("python", conda.AsDependency("python=3.7.4").Name) must_be.Equal("=", conda.AsDependency("python=3.7.4").Qualifier) @@ -28,7 +28,7 @@ func TestCanCompareDependencies(t *testing.T) { first := conda.AsDependency("python") second := conda.AsDependency("python=3.7.7") - third := conda.AsDependency("python=3.7.5") + third := conda.AsDependency("python=3.9.13") fourth := conda.AsDependency("robotframework=3.2") wont_be.True(first.IsExact()) @@ -55,7 +55,7 @@ func TestCanCompareDependencies(t *testing.T) { chosen, err = second.ChooseSpecific(third) wont_be.Nil(err) - must_be.Equal("Wont choose between dependencies: python=3.7.7 vs. python=3.7.5", err.Error()) + must_be.Equal("Wont choose between dependencies: python=3.7.7 vs. python=3.9.13", err.Error()) must_be.Nil(chosen) } diff --git a/conda/config_test.go b/conda/config_test.go index 9e74d0b6..9e6db36d 100644 --- a/conda/config_test.go +++ b/conda/config_test.go @@ -24,7 +24,7 @@ func TestReadingCorrectFileProducesText(t *testing.T) { text, err := conda.ReadConfig("testdata/conda.yaml") must_be.Nil(err) wont_be.Text("", text) - must_be.Equal(167, len(text)) + must_be.Equal(169, len(text)) } func TestUnifyLineWorksCorrectly(t *testing.T) { diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index c750eb04..b77b9504 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.24.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.25.1/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 0646d1f2..21f4c03a 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.24.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.25.1/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index fabece23..c8292bff 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.24.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.25.1/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 7ce79280..4f4e0f21 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -197,7 +197,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 24000 + goodEnough := version >= 25001 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/conda/testdata/conda.yaml b/conda/testdata/conda.yaml index a259ca18..cbc425cc 100644 --- a/conda/testdata/conda.yaml +++ b/conda/testdata/conda.yaml @@ -1,10 +1,10 @@ channels: - - defaults - conda-forge + - defaults dependencies: - - python=3.7.5 + - python=3.9.13 - pip - robotframework=3.1 - robotframework-seleniumlibrary - pip: - - webdrivermanager \ No newline at end of file + - webdrivermanager diff --git a/conda/testdata/other.yaml b/conda/testdata/other.yaml index 99bcdf68..69943fb8 100644 --- a/conda/testdata/other.yaml +++ b/conda/testdata/other.yaml @@ -1,7 +1,7 @@ channels: - defaults dependencies: - - python=3.7.5 + - python=3.9.13 - pip - robotframework=3.2 - robotframework-seleniumlibrary diff --git a/conda/testdata/third.yaml b/conda/testdata/third.yaml index 19ec48ce..6721b269 100644 --- a/conda/testdata/third.yaml +++ b/conda/testdata/third.yaml @@ -1,8 +1,8 @@ channels: - - defaults - conda-forge + - defaults dependencies: - - python=3.7.5 + - python=3.9.13 - pip - robotframework=3.2 - robotframework-seleniumlibrary diff --git a/docs/changelog.md b/docs/changelog.md index 45ebc077..acb26e66 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.16.0 (date: 16.8.2022) + +- micromamba upgrade to v0.25.1 +- template upgrade of python to 3.9.13 +- template upgrade of pip to 22.1.2 +- template upgrade of rpaframework to 15.6.0 +- upgraded tests to match above version changes and their effects + ## v11.15.4 (date: 13.7.2022) - go-bindata was accidentally removed, adding it back diff --git a/docs/recipes.md b/docs/recipes.md index 8afa47da..c84d36cd 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -642,12 +642,12 @@ channels: - conda-forge dependencies: -- python=3.7.5 +- python=3.9.13 - nodejs=16.14.2 -- pip=20.1 +- pip=22.1.2 - pip: - robotframework-browser==12.3.0 - - rpaframework==13.0.0 + - rpaframework==15.6.0 rccPostInstall: - rfbrowser init @@ -680,8 +680,8 @@ But there is also `- pip:` part and those dependenies come from [PyPI](https://pypi.org/) and they are installed after dependencies from `channels:` have been installed. -In above example, `python=3.7.5` comes from `conda-forge` channel. -And `rpaframework==13.0.0` comes from [PyPI](https://pypi.org/project/rpaframework/). +In above example, `python=3.9.13` comes from `conda-forge` channel. +And `rpaframework==15.6.0` comes from [PyPI](https://pypi.org/project/rpaframework/). ### What are `rccPostInstall:` scripts? diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index a6af6efa..795a20c1 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -13,7 +13,7 @@ Github issue 7 about initial call with do-not-track Bug in virtual holotree with gzipped files Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml Use STDERR - Must Have Blueprint "ef0163b57ff44cd5" is available: false + Must Have Blueprint "8b2083d262262cbd" is available: false Step build/rcc run --liveonly --controller citests --robot robot_tests/spellbug/robot.yaml Use STDOUT @@ -21,7 +21,7 @@ Bug in virtual holotree with gzipped files Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml Use STDERR - Must Have Blueprint "ef0163b57ff44cd5" is available: false + Must Have Blueprint "8b2083d262262cbd" is available: false Step build/rcc run --controller citests --robot robot_tests/spellbug/robot.yaml Use STDOUT @@ -29,7 +29,7 @@ Bug in virtual holotree with gzipped files Step build/rcc holotree blueprint --controller citests robot_tests/spellbug/conda.yaml Use STDERR - Must Have Blueprint "ef0163b57ff44cd5" is available: true + Must Have Blueprint "8b2083d262262cbd" is available: true Github issue 32 about rcc task script command failing [Tags] WIP diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 8cf801f5..4a97a057 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -41,18 +41,18 @@ Goal: Must have author space visible Must Have 4e67cd8_fcb4b859 Must Have rcc.citests Must Have author - Must Have 55aacd3b136421fd + Must Have 1cdd0b852854fe5b Wont Have guest Goal: Show exportable environment list Step build/rcc ht export Use STDERR Must Have Selectable catalogs - Must Have - 55aacd3b136421fd + Must Have - 1cdd0b852854fe5b Must Have OK. Goal: Export environment for standalone robot - Step build/rcc ht export -z tmp/standalone/hololib.zip 55aacd3b136421fd + Step build/rcc ht export -z tmp/standalone/hololib.zip 1cdd0b852854fe5b Use STDERR Wont Have Selectable catalogs Must Have OK. @@ -75,7 +75,7 @@ Goal: Can delete author space Wont Have 4e67cd8_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have 55aacd3b136421fd + Wont Have 1cdd0b852854fe5b Wont Have guest Goal: Can run as guest @@ -92,6 +92,6 @@ Goal: Space created under author for guest Wont Have 4e67cd8_fcb4b859 Wont Have author Must Have rcc.citests - Must Have 55aacd3b136421fd + Must Have 1cdd0b852854fe5b Must Have 4e67cd8_aacf1552 Must Have guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index bd077a7b..5b990a78 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -130,14 +130,14 @@ Goal: Merge two different conda.yaml files with conflict fails Must Have robotframework=3.1 vs. robotframework=3.2 Goal: Merge two different conda.yaml files without conflict passes - Step build/rcc holotree vars --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent - Must Have 5bea0c1d2419493e + Step build/rcc holotree vars --controller citests conda/testdata/third.yaml conda/testdata/other.yaml --silent + Must Have RCC_ENVIRONMENT_HASH=ffd32af1fdf0f253 Must Have 4e67cd8_9fcd2534 Goal: Can list environments as JSON Step build/rcc holotree list --controller citests --json Must Have 4e67cd8_9fcd2534 - Must Have 5bea0c1d2419493e + Must Have ffd32af1fdf0f253 Must Be Json Response Goal: See variables from specific environment without robot.yaml knowledge @@ -160,7 +160,7 @@ Goal: See variables from specific environment without robot.yaml knowledge Wont Have PYTHONPATH= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= - Must Have 54399f4561ae95af + Must Have RCC_ENVIRONMENT_HASH=786fd9dca1e1f1db Step build/rcc holotree check --controller citests Goal: See variables from specific environment with robot.yaml but without task @@ -177,7 +177,7 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= - Must Have RCC_ENVIRONMENT_HASH=55aacd3b136421fd + Must Have RCC_ENVIRONMENT_HASH=1cdd0b852854fe5b Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= Must Have PYTHONPATH= diff --git a/robot_tests/spellbug/conda.yaml b/robot_tests/spellbug/conda.yaml index 2132ae45..7b0bedcb 100644 --- a/robot_tests/spellbug/conda.yaml +++ b/robot_tests/spellbug/conda.yaml @@ -1,7 +1,7 @@ channels: - conda-forge dependencies: -- python=3.7.5 -- pip=20.1 +- python=3.9.13 +- pip=22.1.2 - pip: - pyspellchecker==0.6.2 diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index 9cd40bb8..09c06c33 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -12,7 +12,7 @@ Goal: Initialize new standard robot. Goal: Standard robot has correct hash. Step build/rcc holotree hash --silent --controller citests tmp/standardi/conda.yaml - Must Have 55aacd3b136421fd + Must Have 1cdd0b852854fe5b Goal: Running standard robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/standardi/robot.yaml @@ -26,7 +26,7 @@ Goal: Initialize new python robot. Goal: Python robot has correct hash. Step build/rcc holotree hash --silent --controller citests tmp/pythoni/conda.yaml - Must Have 55aacd3b136421fd + Must Have 1cdd0b852854fe5b Goal: Running python robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/pythoni/robot.yaml @@ -40,7 +40,7 @@ Goal: Initialize new extended robot. Goal: Extended robot has correct hash. Step build/rcc holotree hash --silent --controller citests tmp/extendedi/conda.yaml - Must Have 55aacd3b136421fd + Must Have 1cdd0b852854fe5b Goal: Running extended robot is succesful. (Run All Tasks) Step build/rcc task run --space templates --task "Run All Tasks" --controller citests --robot tmp/extendedi/robot.yaml diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index 3bcab5e0..231b1bf2 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -6,10 +6,10 @@ dependencies: # Define conda packages here. # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - - python=3.7.5 + - python=3.9.13 - - pip=20.1 + - pip=22.1.2 - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html + - rpaframework==15.6.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index 3bcab5e0..231b1bf2 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -6,10 +6,10 @@ dependencies: # Define conda packages here. # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - - python=3.7.5 + - python=3.9.13 - - pip=20.1 + - pip=22.1.2 - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html + - rpaframework==15.6.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index 3bcab5e0..231b1bf2 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -6,10 +6,10 @@ dependencies: # Define conda packages here. # If available, always prefer the conda version of a package, installation will be faster and more efficient. # https://anaconda.org/search - - python=3.7.5 + - python=3.9.13 - - pip=20.1 + - pip=22.1.2 - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html + - rpaframework==15.6.0 # https://rpaframework.org/releasenotes.html From 5d90577facd1d9acf89cf6380351ca5c7cbca8eb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 17 Aug 2022 15:54:13 +0300 Subject: [PATCH 280/516] BUGFIX: symbolic link in holotree (v11.17.0) - fix started: adding missing symbolic link handling of files and directories - this will be UNSTABLE, work in progress, for now --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/directory.go | 40 +++++++++++++++++++++++++++------------- pathlib/functions.go | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/common/version.go b/common/version.go index 1aacb095..2fe73ad0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.16.0` + Version = `v11.17.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index acb26e66..90e0fbbe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.17.0 (date: 17.8.2022) UNSTABLE + +- fix started: adding missing symbolic link handling of files and directories +- this will be UNSTABLE, work in progress, for now + ## v11.16.0 (date: 16.8.2022) - micromamba upgrade to v0.25.1 diff --git a/htfs/directory.go b/htfs/directory.go index 32115e97..f77a0d8b 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -60,7 +60,7 @@ func NewRoot(path string) (*Root, error) { Path: fullpath, Platform: common.Platform(), Lifted: false, - Tree: newDir(""), + Tree: newDir("", ""), }, nil } @@ -183,10 +183,15 @@ func (it *Root) LoadFrom(filename string) error { } type Dir struct { - Name string `json:"name"` - Mode fs.FileMode `json:"mode"` - Dirs map[string]*Dir `json:"subdirs"` - Files map[string]*File `json:"files"` + Name string `json:"name"` + Symlink string `json:"symlink,omitempty"` + Mode fs.FileMode `json:"mode"` + Dirs map[string]*Dir `json:"subdirs"` + Files map[string]*File `json:"files"` +} + +func (it *Dir) IsSymlink() bool { + return len(it.Symlink) > 0 } func (it *Dir) AllDirs(path string, task Dirtask) { @@ -222,16 +227,18 @@ func (it *Dir) Lift(path string) error { if killfile[part.Name()] || killfile[filepath.Ext(part.Name())] { continue } + fullpath := filepath.Join(path, part.Name()) // following must be done to get by symbolic links - info, err := os.Stat(filepath.Join(path, part.Name())) + info, err := os.Stat(fullpath) if err != nil { return err } + symlink, _ := pathlib.Symlink(fullpath) if info.IsDir() { - it.Dirs[part.Name()] = newDir(info.Name()) + it.Dirs[part.Name()] = newDir(info.Name(), symlink) continue } - it.Files[part.Name()] = newFile(info) + it.Files[part.Name()] = newFile(info, symlink) } for name, dir := range it.Dirs { err = dir.Lift(filepath.Join(path, name)) @@ -244,12 +251,17 @@ func (it *Dir) Lift(path string) error { type File struct { Name string `json:"name"` + Symlink string `json:"symlink,omitempty"` Size int64 `json:"size"` Mode fs.FileMode `json:"mode"` Digest string `json:"digest"` Rewrite []int64 `json:"rewrite"` } +func (it *File) IsSymlink() bool { + return len(it.Symlink) > 0 +} + func (it *File) Match(info fs.FileInfo) bool { name := it.Name == info.Name() size := it.Size == info.Size() @@ -257,17 +269,19 @@ func (it *File) Match(info fs.FileInfo) bool { return name && size && mode } -func newDir(name string) *Dir { +func newDir(name, symlink string) *Dir { return &Dir{ - Name: name, - Dirs: make(map[string]*Dir), - Files: make(map[string]*File), + Name: name, + Symlink: symlink, + Dirs: make(map[string]*Dir), + Files: make(map[string]*File), } } -func newFile(info fs.FileInfo) *File { +func newFile(info fs.FileInfo, symlink string) *File { return &File{ Name: info.Name(), + Symlink: symlink, Mode: info.Mode(), Size: info.Size(), Digest: "N/A", diff --git a/pathlib/functions.go b/pathlib/functions.go index 3299ebef..7f708f88 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -23,6 +23,22 @@ func Abs(path string) (string, error) { return filepath.Clean(fullpath), nil } +func Symlink(pathname string) (string, bool) { + stat, err := os.Lstat(pathname) + if err != nil { + return "", false + } + mode := stat.Mode() + if mode&fs.ModeSymlink == 0 { + return "", false + } + name, err := os.Readlink(pathname) + if err != nil { + return "", false + } + return name, true +} + func IsDir(pathname string) bool { stat, err := os.Stat(pathname) return err == nil && stat.IsDir() From 8343068a4a88c3d31da881ea16be00fb5401fb11 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 18 Aug 2022 13:19:46 +0300 Subject: [PATCH 281/516] BUGFIX: symbolic link in holotree (v11.17.1) - fix continued: adding missing symbolic link handling of files and directories --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/directory.go | 9 ++++++--- htfs/functions.go | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 2fe73ad0..8fc8bfa4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.17.0` + Version = `v11.17.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 90e0fbbe..c397518a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.17.1 (date: 18.8.2022) UNSTABLE + +- fix continued: adding missing symbolic link handling of files and directories + ## v11.17.0 (date: 17.8.2022) UNSTABLE - fix started: adding missing symbolic link handling of files and directories diff --git a/htfs/directory.go b/htfs/directory.go index f77a0d8b..1de54cd3 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -60,7 +60,7 @@ func NewRoot(path string) (*Root, error) { Path: fullpath, Platform: common.Platform(), Lifted: false, - Tree: newDir("", ""), + Tree: newDir("", "", false), }, nil } @@ -188,6 +188,7 @@ type Dir struct { Mode fs.FileMode `json:"mode"` Dirs map[string]*Dir `json:"subdirs"` Files map[string]*File `json:"files"` + Shadow bool `json:"shadow,omitempty"` } func (it *Dir) IsSymlink() bool { @@ -223,6 +224,7 @@ func (it *Dir) Lift(path string) error { if err != nil { return err } + shadow := it.Shadow || it.IsSymlink() for _, part := range content { if killfile[part.Name()] || killfile[filepath.Ext(part.Name())] { continue @@ -235,7 +237,7 @@ func (it *Dir) Lift(path string) error { } symlink, _ := pathlib.Symlink(fullpath) if info.IsDir() { - it.Dirs[part.Name()] = newDir(info.Name(), symlink) + it.Dirs[part.Name()] = newDir(info.Name(), symlink, shadow) continue } it.Files[part.Name()] = newFile(info, symlink) @@ -269,12 +271,13 @@ func (it *File) Match(info fs.FileInfo) bool { return name && size && mode } -func newDir(name, symlink string) *Dir { +func newDir(name, symlink string, shadow bool) *Dir { return &Dir{ Name: name, Symlink: symlink, Dirs: make(map[string]*Dir), Files: make(map[string]*File), + Shadow: shadow, } } diff --git a/htfs/functions.go b/htfs/functions.go index 068eb679..ad40b722 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -144,6 +144,24 @@ func Locator(seek string) Filetask { } func MakeBranches(path string, it *Dir) error { + if it.IsSymlink() { + anywork.OnErrPanicCloseAll(restoreSymlink(it.Symlink, path)) + return nil + } + hasSymlinks := false +detector: + for _, subdir := range it.Dirs { + if subdir.IsSymlink() { + hasSymlinks = true + break detector + } + } + if hasSymlinks { + err := os.MkdirAll(path, 0o750) + if err != nil { + return err + } + } for _, subdir := range it.Dirs { err := MakeBranches(filepath.Join(path, subdir.Name), subdir) if err != nil { @@ -163,10 +181,16 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { var scheduler Treetop seen := make(map[string]bool) scheduler = func(path string, it *Dir) error { + if it.IsSymlink() { + return nil + } for name, subdir := range it.Dirs { scheduler(filepath.Join(path, name), subdir) } for name, file := range it.Files { + if file.IsSymlink() { + continue + } if seen[file.Digest] { common.Trace("LiftFile %s %q already scheduled.", file.Digest, name) continue @@ -270,6 +294,10 @@ func LiftFile(sourcename, sinkname string) anywork.Work { func DropFile(library Library, digest, sinkname string, details *File, rewrite []byte) anywork.Work { return func() { + if details.IsSymlink() { + anywork.OnErrPanicCloseAll(restoreSymlink(details.Symlink, sinkname)) + return + } reader, closer, err := library.Open(digest) anywork.OnErrPanicCloseAll(err) @@ -352,13 +380,29 @@ func CalculateTreeStats() (Dirtask, *TreeStats) { }, result } +func restoreSymlink(source, target string) error { + old, ok := pathlib.Symlink(target) + if ok && old == target { + return nil + } + os.Remove(target) + return os.Symlink(source, target) +} + func RestoreDirectory(library Library, fs *Root, current map[string]string, stats *stats) Dirtask { return func(path string, it *Dir) anywork.Work { return func() { - content, err := os.ReadDir(path) + if it.Shadow { + return + } + if it.IsSymlink() { + anywork.OnErrPanicCloseAll(restoreSymlink(it.Symlink, path)) + return + } + existingEntries, err := os.ReadDir(path) anywork.OnErrPanicCloseAll(err) files := make(map[string]bool) - for _, part := range content { + for _, part := range existingEntries { directpath := filepath.Join(path, part.Name()) if part.IsDir() { _, ok := it.Dirs[part.Name()] From 3be69abeedc4198ab45f7c984db3d99524f67e38 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 19 Aug 2022 08:47:43 +0300 Subject: [PATCH 282/516] BUGFIX: symbolic link in holotree (v11.17.2) - bugfix: adding missing symbolic link handling of files and directories - hololib catalogs now have rcc version information included - added timeout to account deletion, to speed up unit tests --- cmd/credentials.go | 3 ++- common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/directory.go | 12 +++++++----- operations/credentials.go | 3 ++- operations/credentials_test.go | 3 ++- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/credentials.go b/cmd/credentials.go index bd4953a0..4e60bad3 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -2,6 +2,7 @@ package cmd import ( "strings" + "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -70,7 +71,7 @@ func localDelete(accountName string) { if account == nil { pretty.Exit(1, "Could not find account by name: %q", accountName) } - err := account.Delete() + err := account.Delete(10 * time.Second) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/common/version.go b/common/version.go index 8fc8bfa4..7ed375a9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.17.1` + Version = `v11.17.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index c397518a..3926839c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.17.2 (date: 19.8.2022) + +- bugfix: adding missing symbolic link handling of files and directories +- hololib catalogs now have rcc version information included +- added timeout to account deletion, to speed up unit tests + ## v11.17.1 (date: 18.8.2022) UNSTABLE - fix continued: adding missing symbolic link handling of files and directories diff --git a/htfs/directory.go b/htfs/directory.go index 1de54cd3..fd0b7a2b 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -39,6 +39,7 @@ type Dirtask func(string, *Dir) anywork.Work type Treetop func(string, *Dir) error type Root struct { + RccVersion string `json:"rcc"` Identity string `json:"identity"` Path string `json:"path"` Controller string `json:"controller"` @@ -56,11 +57,12 @@ func NewRoot(path string) (*Root, error) { } basename := filepath.Base(fullpath) return &Root{ - Identity: basename, - Path: fullpath, - Platform: common.Platform(), - Lifted: false, - Tree: newDir("", "", false), + Identity: basename, + Path: fullpath, + Platform: common.Platform(), + Lifted: false, + Tree: newDir("", "", false), + RccVersion: common.Version, }, nil } diff --git a/operations/credentials.go b/operations/credentials.go index 8c0c94bf..e10ec921 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -132,7 +132,7 @@ func (it *account) Cached(name, url string) (string, bool) { return found.Token, true } -func (it *account) Delete() error { +func (it *account) Delete(timeout time.Duration) error { prefix := accountsPrefix + it.Account defer xviper.Set(prefix, "deleted") @@ -140,6 +140,7 @@ func (it *account) Delete() error { if err != nil { return err } + client = client.WithTimeout(timeout) return DeleteAccount(client, it) } diff --git a/operations/credentials_test.go b/operations/credentials_test.go index 8dd474e8..7a32712c 100644 --- a/operations/credentials_test.go +++ b/operations/credentials_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/robocorp/rcc/hamlet" "github.com/robocorp/rcc/operations" @@ -66,7 +67,7 @@ func TestCanCreateAndDeleteAccount(t *testing.T) { wont_be.Nil(sut) must_be.True(strings.HasSuffix(xviper.ConfigFileUsed(), "rcctest.yaml")) must_be.Equal("42.long_a", sut.CacheKey()) - sut.Delete() + sut.Delete(50 * time.Millisecond) sut = operations.AccountByName("dele") must_be.Nil(sut) } From 9ffd81ce1d6265d905385607d7f1e319334ea41a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 23 Aug 2022 11:15:00 +0300 Subject: [PATCH 283/516] UPDATE: cleanup updates (v11.18.0) - new cleanup option `--downloads` to remove downloads caches (conda, pip, and templates) - change: now conda pkgs is cleaned up also in quick cleanup (which now includes all "downloads" cleanups) - robot cache is now part of full cleanup - run commands now cleanup their temp folders immediately --- cmd/assistantRun.go | 2 ++ cmd/cleanup.go | 6 ++++-- cmd/rcc/main.go | 11 +++++++---- cmd/run.go | 2 ++ cmd/testrun.go | 2 ++ common/variables.go | 6 +++++- common/version.go | 2 +- conda/cleanup.go | 38 +++++++++++++++++++++++++++++++------- docs/changelog.md | 9 +++++++++ robot_tests/holotree.robot | 3 ++- 10 files changed, 65 insertions(+), 16 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 78ec6516..8bcb5d4a 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -23,6 +24,7 @@ var assistantRunCmd = &cobra.Command{ Short: "Robot Assistant run", Long: "Robot Assistant run.", Run: func(cmd *cobra.Command, args []string) { + defer conda.RemoveCurrentTemp() var status, reason string status, reason = "ERROR", "UNKNOWN" elapser := common.Stopwatch("Robot Assistant startup lasted") diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 97270a7f..24a79419 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -12,6 +12,7 @@ var ( allFlag bool quickFlag bool micromambaFlag bool + downloadsFlag bool daysOption int ) @@ -24,7 +25,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag) + err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -38,5 +39,6 @@ func init() { cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "", false, "Cleanup all enviroments.") cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") - cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") + cleanupCmd.Flags().BoolVarP(&downloadsFlag, "downloads", "", false, "Cleanup downloaded cache files (pip/conda/templates)") + cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep temp folders (deletes directories older than this).") } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index d786369d..25d90341 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -80,10 +80,13 @@ func markTempForRecycling() { if markedAlready { return } - markedAlready = true - filename := filepath.Join(common.RobocorpTemp(), "recycle.now") - ioutil.WriteFile(filename, []byte("True"), 0o644) - common.Debug("Marked %q for recycling.", common.RobocorpTemp()) + target := common.RobocorpTempName() + if pathlib.Exists(target) { + filename := filepath.Join(target, "recycle.now") + ioutil.WriteFile(filename, []byte("True"), 0o644) + common.Debug("Marked %q for recycling.", target) + markedAlready = true + } } func main() { diff --git a/cmd/run.go b/cmd/run.go index 5ba04bc5..dfd1141b 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/xviper" @@ -22,6 +23,7 @@ var runCmd = &cobra.Command{ Long: `Local task run, in place, to see how full run execution works in your own machine.`, Run: func(cmd *cobra.Command, args []string) { + defer conda.RemoveCurrentTemp() if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() } diff --git a/cmd/testrun.go b/cmd/testrun.go index 7ce9ded2..3160b7b6 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -23,6 +24,7 @@ var testrunCmd = &cobra.Command{ Short: "Run a task in a clean environment and clean directory.", Long: "Run a task in a clean environment and clean directory.", Run: func(cmd *cobra.Command, args []string) { + defer conda.RemoveCurrentTemp() if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } diff --git a/common/variables.go b/common/variables.go index 6ca274c4..394ad403 100644 --- a/common/variables.go +++ b/common/variables.go @@ -104,8 +104,12 @@ func RobocorpTempRoot() string { return filepath.Join(RobocorpHome(), "temp") } +func RobocorpTempName() string { + return filepath.Join(RobocorpTempRoot(), randomIdentifier) +} + func RobocorpTemp() string { - tempLocation := filepath.Join(RobocorpTempRoot(), randomIdentifier) + tempLocation := RobocorpTempName() fullpath, err := filepath.Abs(tempLocation) if err != nil { fullpath = tempLocation diff --git a/common/version.go b/common/version.go index 7ed375a9..9908fe7f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.17.2` + Version = `v11.18.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 908019c1..638953dd 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -57,16 +57,26 @@ func alwaysCleanup(dryrun bool) { safeRemove("legacy", miniconda3) } -func quickCleanup(dryrun bool) error { +func downloadCleanup(dryrun bool) error { if dryrun { common.Log("- %v", common.TemplateLocation()) common.Log("- %v", common.PipCache()) + common.Log("- %v", common.MambaPackages()) + } else { + safeRemove("templates", common.TemplateLocation()) + safeRemove("cache", common.PipCache()) + safeRemove("cache", common.MambaPackages()) + } + return nil +} + +func quickCleanup(dryrun bool) error { + downloadCleanup(dryrun) + if dryrun { common.Log("- %v", common.HolotreeLocation()) common.Log("- %v", common.RobocorpTempRoot()) return nil } - safeRemove("templates", common.TemplateLocation()) - safeRemove("cache", common.PipCache()) err := safeRemove("cache", common.HolotreeLocation()) if err != nil { return err @@ -80,13 +90,13 @@ func spotlessCleanup(dryrun bool) error { return err } if dryrun { - common.Log("- %v", common.MambaPackages()) common.Log("- %v", BinMicromamba()) + common.Log("- %v", common.RobotCache()) common.Log("- %v", common.HololibLocation()) return nil } - safeRemove("cache", common.MambaPackages()) safeRemove("executable", BinMicromamba()) + safeRemove("cache", common.RobotCache()) return safeRemove("cache", common.HololibLocation()) } @@ -123,7 +133,7 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { return nil } -func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { +func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error { lockfile := common.RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -134,6 +144,10 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { alwaysCleanup(dryrun) + if downloads { + return downloadCleanup(dryrun) + } + if quick { return quickCleanup(dryrun) } @@ -142,7 +156,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { return spotlessCleanup(dryrun) } - deadline := time.Now().Add(-48 * time.Duration(daylimit) * time.Hour) + deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) cleanupTemp(deadline, dryrun) if micromamba && err == nil { @@ -153,3 +167,13 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { } return err } + +func RemoveCurrentTemp() { + target := common.RobocorpTempName() + common.Debug("removing current temp %v", target) + common.Timeline("removing current temp: %v", target) + err := safeRemove("temp", target) + if err != nil { + common.Timeline("removing current temp failed, reason: %v", err) + } +} diff --git a/docs/changelog.md b/docs/changelog.md index 3926839c..b76ca2fd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.18.0 (date: 23.8.2022) + +- new cleanup option `--downloads` to remove downloads caches (conda, pip, + and templates) +- change: now conda pkgs is cleaned up also in quick cleanup (which now + includes all "downloads" cleanups) +- robot cache is now part of full cleanup +- run commands now cleanup their temp folders immediately + ## v11.17.2 (date: 19.8.2022) - bugfix: adding missing symbolic link handling of files and directories diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index cae95276..b81d413e 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -126,8 +126,9 @@ Goal: Liveonly works and uses virtual holotree Goal: Do quick cleanup on environments Step build/rcc config cleanup --controller citests --quick Must Exist %{ROBOCORP_HOME}/bin/micromamba - Must Exist %{ROBOCORP_HOME}/pkgs/ + Wont Exist %{ROBOCORP_HOME}/pkgs/ Wont Exist %{ROBOCORP_HOME}/pipcache/ + Wont Exist %{ROBOCORP_HOME}/templates/ Use STDERR Must Have OK From fd644f7b80927593df91f682116a4ce6338c3a6c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 24 Aug 2022 09:12:50 +0300 Subject: [PATCH 284/516] FEATURE: environment no-build option (v11.19.0) - new global flag `--no-build` which prevents building environments, and only allows using previously cached, prebuild or imported holotrees - there is also "no-build" option in "settings.yaml" options section - added "no-build" information to diagnostics output --- assets/settings.yaml | 3 +++ cmd/root.go | 1 + common/variables.go | 1 + common/version.go | 2 +- docs/changelog.md | 7 +++++++ htfs/commands.go | 6 ++++++ operations/diagnostics.go | 1 + settings/api.go | 2 ++ settings/data.go | 6 ++++++ settings/settings.go | 9 +++++++++ 10 files changed, 37 insertions(+), 1 deletion(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index 6077c26d..cbe8b895 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -27,6 +27,9 @@ certificates: verify-ssl: true ssl-no-revoke: false +options: + no-build: false + network: https-proxy: # no proxy by default http-proxy: # no proxy by default diff --git a/cmd/root.go b/cmd/root.go index e6378e83..bb2bcbf6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -98,6 +98,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") + rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib") rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") rootCmd.PersistentFlags().BoolVarP(&pathlib.Lockless, "lockless", "", false, "do not use file locking ... DANGER!") diff --git a/common/variables.go b/common/variables.go index 394ad403..f127c19e 100644 --- a/common/variables.go +++ b/common/variables.go @@ -17,6 +17,7 @@ const ( ) var ( + NoBuild bool Silent bool DebugFlag bool TraceFlag bool diff --git a/common/version.go b/common/version.go index 9908fe7f..cd422acc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.18.0` + Version = `v11.19.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index b76ca2fd..7296b49c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.19.0 (date: 24.8.2022) + +- new global flag `--no-build` which prevents building environments, and + only allows using previously cached, prebuild or imported holotrees +- there is also "no-build" option in "settings.yaml" options section +- added "no-build" information to diagnostics output + ## v11.18.0 (date: 23.8.2022) - new cleanup option `--downloads` to remove downloads caches (conda, pip, diff --git a/htfs/commands.go b/htfs/commands.go index 74b4986e..cc452868 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -13,12 +13,17 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" + "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) func NewEnvironment(condafile, holozip string, restore, force bool) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) + if settings.Global.NoBuild() { + pretty.Note("'no-build' setting is active. Only cached, prebuild, or imported environments are allowed!") + } + haszip := len(holozip) > 0 if haszip { common.Debug("New zipped environment from %q!", holozip) @@ -102,6 +107,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec if force || !exists { common.Progress(3, "Cleanup holotree stage for fresh install.") + fail.On(settings.Global.NoBuild(), "Building new holotree environment is blocked by settings, and could not be found from hololib cache!") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 32122499..e818c6a8 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -96,6 +96,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") + result.Details["no-build"] = fmt.Sprintf("%v", settings.Global.NoBuild()) who, err := user.Current() if err == nil { diff --git a/settings/api.go b/settings/api.go index f08af76e..d7f2dee1 100644 --- a/settings/api.go +++ b/settings/api.go @@ -14,6 +14,7 @@ type Api interface { TemplatesYamlURL() string Diagnostics(target *common.DiagnosticStatus) Endpoint(string) string + Option(string) bool DefaultEndpoint() string IssuesURL() string TelemetryURL() string @@ -33,4 +34,5 @@ type Api interface { HasCaBundle() bool VerifySsl() bool NoRevocation() bool + NoBuid() bool } diff --git a/settings/data.go b/settings/data.go index 46e45e86..03a3a19c 100644 --- a/settings/data.go +++ b/settings/data.go @@ -16,6 +16,7 @@ const ( ) type StringMap map[string]string +type BoolMap map[string]bool func (it StringMap) Lookup(key string) string { return it[key] @@ -33,6 +34,7 @@ func (it SettingsLayers) Effective() *Settings { Certificates: &Certificates{}, Network: &Network{}, Endpoints: make(StringMap), + Options: make(BoolMap), Hosts: make([]string, 0, 100), Meta: &Meta{ Name: "generated", @@ -56,6 +58,7 @@ type Settings struct { Network *Network `yaml:"network,omitempty" json:"network,omitempty"` Endpoints StringMap `yaml:"endpoints,omitempty" json:"endpoints,omitempty"` Hosts []string `yaml:"diagnostics-hosts,omitempty" json:"diagnostics-hosts,omitempty"` + Options BoolMap `yaml:"options,omitempty" json:"options,omitempty"` Meta *Meta `yaml:"meta,omitempty" json:"meta,omitempty"` } @@ -95,6 +98,9 @@ func (it *Settings) onTopOf(target *Settings) { target.Endpoints[key] = value } } + for key, value := range it.Options { + target.Options[key] = value + } for _, host := range it.Hosts { target.Hosts = append(target.Hosts, host) } diff --git a/settings/settings.go b/settings/settings.go index 5fe2c21d..4887e77b 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -157,6 +157,11 @@ func (it gateway) Endpoint(key string) string { return it.settings().Endpoints[key] } +func (it gateway) Option(key string) bool { + value, ok := it.settings().Options[key] + return ok && value +} + func (it gateway) DefaultEndpoint() string { return it.Endpoint("cloud-api") } @@ -235,6 +240,10 @@ func (it gateway) NoRevocation() bool { return it.settings().Certificates.SslNoRevoke } +func (it gateway) NoBuild() bool { + return common.NoBuild || it.Option("no-build") +} + func (it gateway) ConfiguredHttpTransport() *http.Transport { return httpTransport } From 49f1630b8b56cd333697b972ed18ed7c43e1218e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 25 Aug 2022 11:26:39 +0300 Subject: [PATCH 285/516] BUGFIX: empty ignoreFiles entry bug (v11.19.1) - bug: empty entry on ignoreFiles caused unclear error - fix: now empty entries are diagnosed and noted - fix: also non-existing ignore files are diagnosed --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ robot/robot.go | 25 ++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index cd422acc..63b8cceb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.19.0` + Version = `v11.19.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7296b49c..9dd59a1c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.19.1 (date: 25.8.2022) + +- bug: empty entry on ignoreFiles caused unclear error +- fix: now empty entries are diagnosed and noted +- fix: also non-existing ignore files are diagnosed + ## v11.19.0 (date: 24.8.2022) - new global flag `--no-build` which prevents building environments, and diff --git a/robot/robot.go b/robot/robot.go index 3005abc7..fdb503c7 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -156,12 +156,23 @@ func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { diagnose.Warning("", "No ignoreFiles defined, so everything ends up inside robot.zip file.") ok = false } else { - for _, path := range it.Ignored { + for at, path := range it.Ignored { + if len(strings.TrimSpace(path)) == 0 { + diagnose.Fail("", "there is empty entry in ignoreFiles at position %d", at+1) + ok = false + continue + } if filepath.IsAbs(path) { diagnose.Fail("", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } } + for _, path := range it.IgnoreFiles() { + if !pathlib.IsFile(path) { + diagnose.Fail("", "ignoreFiles entry %q is not a file.", path) + ok = false + } + } } if ok { diagnose.Ok("ignoreFiles settings in robot.yaml are ok.") @@ -292,8 +303,16 @@ func (it *robot) IgnoreFiles() []string { return []string{} } result := make([]string, 0, len(it.Ignored)) - for _, entry := range it.Ignored { - result = append(result, filepath.Join(it.Root, entry)) + for at, entry := range it.Ignored { + if len(strings.TrimSpace(entry)) == 0 { + pretty.Warning("Ignore file entry at position %d is empty string!", at+1) + continue + } + fullpath := filepath.Join(it.Root, entry) + if !pathlib.IsFile(fullpath) { + pretty.Warning("Ignore file %q is not a file!", fullpath) + } + result = append(result, fullpath) } return result } From 1c509b7125f415eb32b7ce4e75ee7a16959e8c91 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 26 Aug 2022 10:34:55 +0300 Subject: [PATCH 286/516] FEATURE: holotree export by robot.yaml (v11.20.0) - feature: allow holotree exporting using robot.yaml file. --- cmd/holotreeExport.go | 10 +++++++++- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/library.go | 11 +++++++++-- htfs/ziplibrary.go | 2 +- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index c4c2c09f..86f3eb83 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -11,7 +11,8 @@ import ( ) var ( - holozip string + holozip string + exportRobot string ) func holotreeExport(catalogs []string, archive string) { @@ -61,6 +62,12 @@ var holotreeExportCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree export command lasted").Report() } + if len(exportRobot) > 0 { + _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, exportRobot) + pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) + hash := htfs.BlueprintHash(holotreeBlueprint) + args = append(args, htfs.CatalogName(hash)) + } if len(args) == 0 { listCatalogs(jsonFlag) } else { @@ -74,4 +81,5 @@ func init() { holotreeCmd.AddCommand(holotreeExportCmd) holotreeExportCmd.Flags().StringVarP(&holozip, "zipfile", "z", "hololib.zip", "Name of zipfile to export.") holotreeExportCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + holotreeExportCmd.Flags().StringVarP(&exportRobot, "robot", "r", "", "Full path to 'robot.yaml' configuration file to export as catalog. ") } diff --git a/common/version.go b/common/version.go index 63b8cceb..99cd19d2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.19.1` + Version = `v11.20.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9dd59a1c..aec208a3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.20.0 (date: 26.8.2022) + +- feature: allow holotree exporting using robot.yaml file. + ## v11.19.1 (date: 25.8.2022) - bug: empty entry on ignoreFiles caused unclear error diff --git a/htfs/library.go b/htfs/library.go index cf56e330..079aa273 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -134,6 +134,8 @@ func (it *hololib) Export(catalogs []string, archive string) (err error) { make(map[string]bool), } + exported := false + for _, name := range catalogs { catalog := filepath.Join(common.HololibCatalogLocation(), name) relative, err := filepath.Rel(common.HololibLocation(), catalog) @@ -147,7 +149,9 @@ func (it *hololib) Export(catalogs []string, archive string) (err error) { fail.On(err != nil, "Could not load catalog from %s -> %v.", catalog, err) err = fs.Treetop(ZipRoot(it, fs, zipper)) fail.On(err != nil, "Could not zip catalog %s -> %v.", catalog, err) + exported = true } + fail.On(!exported, "None of given catalogs were available for export!") return nil } @@ -184,9 +188,12 @@ func (it *hololib) Record(blueprint []byte) error { return err } +func CatalogName(key string) string { + return fmt.Sprintf("%sv12.%s", key, common.Platform()) +} + func (it *hololib) CatalogPath(key string) string { - name := fmt.Sprintf("%sv12.%s", key, common.Platform()) - return filepath.Join(common.HololibCatalogLocation(), name) + return filepath.Join(common.HololibCatalogLocation(), CatalogName(key)) } func (it *hololib) HasBlueprint(blueprint []byte) bool { diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index ff2d157a..872ace79 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -71,7 +71,7 @@ func (it *ziplibrary) Open(digest string) (readable io.Reader, closer Closer, er } func (it *ziplibrary) CatalogPath(key string) string { - return filepath.Join("catalog", fmt.Sprintf("%sv12.%s", key, common.Platform())) + return filepath.Join("catalog", CatalogName(key)) } func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { From 2f89c0213e25d0878479c609ebb0343a6e5b7b6f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 30 Aug 2022 10:49:02 +0300 Subject: [PATCH 287/516] FEATURE: catalog usage improvements (v11.21.0) - added support to tracking when catalog blueprints are used - if there is no tracking info on existing catalog, first reporting will reset it to zero (and report it as -1) - added catalog age in days, and days since last used to catalog listing - fixed bug on shared hololib location on catalog listing --- cmd/holotreeCatalogs.go | 52 +++++++++++++++++++++++++++++++++++++---- common/algorithms.go | 7 ++++++ common/variables.go | 8 +++++-- common/version.go | 2 +- docs/changelog.md | 8 +++++++ htfs/directory.go | 5 ++++ htfs/functions.go | 2 +- htfs/library.go | 7 ++++++ pathlib/functions.go | 9 +++++++ pathlib/touch.go | 7 ++++++ 10 files changed, 99 insertions(+), 8 deletions(-) diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index 39cd46cb..8ba86e5f 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -19,20 +20,55 @@ func megas(bytes uint64) uint64 { return bytes / mega } +func catalogUsedStats() map[string]int { + result := make(map[string]int) + handle, err := os.Open(common.HololibUsageLocation()) + if err != nil { + return result + } + defer handle.Close() + entries, err := handle.Readdir(-1) + if err != nil { + return result + } + for _, entry := range entries { + name := filepath.Base(entry.Name()) + tail := filepath.Ext(name) + size := len(name) - len(tail) + base := name[:size] + days := common.DayCountSince(entry.ModTime()) + previous, ok := result[base] + if !ok || days < previous { + result[base] = days + } + } + return result +} + func jsonCatalogDetails(roots []*htfs.Root) { + used := catalogUsedStats() holder := make(map[string]map[string]interface{}) for _, catalog := range roots { + lastUse, ok := used[catalog.Blueprint] + if !ok { + catalog.Touch() + lastUse = -1 + } stats, err := catalog.Stats() pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) data := make(map[string]interface{}) data["blueprint"] = catalog.Blueprint data["holotree"] = catalog.HolotreeBase() - data["identity.yaml"] = filepath.Join(common.RobocorpHome(), stats.Identity) + identity := filepath.Join(common.HololibLibraryLocation(), stats.Identity) + data["identity.yaml"] = identity data["platform"] = catalog.Platform data["directories"] = stats.Directories data["files"] = stats.Files data["bytes"] = stats.Bytes holder[catalog.Blueprint] = data + age, _ := pathlib.DaysSinceModified(identity) + data["age_in_days"] = age + data["days_since_last_use"] = lastUse } nice, err := json.MarshalIndent(holder, "", " ") pretty.Guard(err == nil, 2, "%s", err) @@ -40,13 +76,21 @@ func jsonCatalogDetails(roots []*htfs.Root) { } func listCatalogDetails(roots []*htfs.Root) { + used := catalogUsedStats() tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) - tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside ROBOCORP_HOME)\tHolotree path\n")) - tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t-------------------------------------------------\t-------------\n")) + tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside hololib)\tHolotree path\tAge (days)\tIdle (days)\n")) + tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t-------------------------------------------\t-------------\t----------\t-----------\n")) for _, catalog := range roots { + lastUse, ok := used[catalog.Blueprint] + if !ok { + catalog.Touch() + lastUse = -1 + } stats, err := catalog.Stats() pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) - data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase()) + identity := filepath.Join(common.HololibLibraryLocation(), stats.Identity) + days, _ := pathlib.DaysSinceModified(identity) + data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\t%10d\t%11d\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase(), days, lastUse) tabbed.Write([]byte(data)) } tabbed.Flush() diff --git a/common/algorithms.go b/common/algorithms.go index 316a6dca..bb03e94f 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "fmt" "math" + "time" "github.com/dchest/siphash" ) @@ -43,3 +44,9 @@ func ShortDigest(content string) string { func Siphash(left, right uint64, body []byte) uint64 { return siphash.Hash(left, right, body) } + +func DayCountSince(timestamp time.Time) int { + duration := time.Since(timestamp) + days := math.Floor(duration.Hours() / 24.0) + return int(days) +} diff --git a/common/variables.go b/common/variables.go index f127c19e..6087e0fe 100644 --- a/common/variables.go +++ b/common/variables.go @@ -47,8 +47,8 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) - // Note: HololibCatalogLocation and HololibLibraryLocation are force - // created from "htfs" direcotry.go init function + // Note: HololibCatalogLocation, HololibLibraryLocation and HololibUsageLocation + // are force created from "htfs" direcotry.go init function // Also: HolotreeLocation creation is left for actual holotree commands // to prevent accidental access right problem during usage @@ -168,6 +168,10 @@ func HololibLibraryLocation() string { return filepath.Join(HololibLocation(), "library") } +func HololibUsageLocation() string { + return filepath.Join(HololibLocation(), "used") +} + func HolotreeLock() string { return fmt.Sprintf("%s.lck", HolotreeLocation()) } diff --git a/common/version.go b/common/version.go index 99cd19d2..54b828c2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.20.0` + Version = `v11.21.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index aec208a3..5ec5385d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.21.0 (date: 29.8.2022) + +- added support to tracking when catalog blueprints are used +- if there is no tracking info on existing catalog, first reporting will + reset it to zero (and report it as -1) +- added catalog age in days, and days since last used to catalog listing +- fixed bug on shared hololib location on catalog listing + ## v11.20.0 (date: 26.8.2022) - feature: allow holotree exporting using robot.yaml file. diff --git a/htfs/directory.go b/htfs/directory.go index fd0b7a2b..89e3eb42 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -32,6 +32,7 @@ func init() { pathlib.MakeSharedDir(common.HoloLocation()) pathlib.MakeSharedDir(common.HololibCatalogLocation()) pathlib.MakeSharedDir(common.HololibLibraryLocation()) + pathlib.MakeSharedDir(common.HololibUsageLocation()) } type Filetask func(string, *File) anywork.Work @@ -66,6 +67,10 @@ func NewRoot(path string) (*Root, error) { }, nil } +func (it *Root) Touch() { + touchUsedHash(it.Blueprint) +} + func (it *Root) HolotreeBase() string { return filepath.Dir(it.Path) } diff --git a/htfs/functions.go b/htfs/functions.go index ad40b722..31f245f2 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -359,7 +359,7 @@ type TreeStats struct { } func guessLocation(digest string) string { - return filepath.Join("hololib", "library", digest[:2], digest[2:4], digest[4:6], digest) + return filepath.Join(digest[:2], digest[2:4], digest[4:6], digest) } func CalculateTreeStats() (Dirtask, *TreeStats) { diff --git a/htfs/library.go b/htfs/library.go index 079aa273..783c2eb8 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -275,6 +275,12 @@ func ControllerSpaceName(client, tag []byte) string { return common.UserHomeIdentity() + "_" + prefix + "_" + suffix } +func touchUsedHash(hash string) { + filename := fmt.Sprintf("%s.%s", hash, common.UserHomeIdentity()) + fullpath := filepath.Join(common.HololibUsageLocation(), filename) + pathlib.ForceTouchWhen(fullpath, common.ProgressMark) +} + func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() @@ -338,6 +344,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er if pathlib.FileExist(identityfile) { common.Log("%sEnvironment configuration descriptor is: %v%s", pretty.Yellow, identityfile, pretty.Reset) } + touchUsedHash(key) return targetdir, nil } diff --git a/pathlib/functions.go b/pathlib/functions.go index 7f708f88..9188e2af 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" ) @@ -49,6 +50,14 @@ func IsFile(pathname string) bool { return err == nil && !stat.IsDir() } +func DaysSinceModified(filename string) (int, error) { + stat, err := os.Stat(filename) + if err != nil { + return -1, err + } + return common.DayCountSince(stat.ModTime()), nil +} + func Size(pathname string) (int64, bool) { stat, err := os.Stat(pathname) if err != nil { diff --git a/pathlib/touch.go b/pathlib/touch.go index ca7017dc..dc7d0c1a 100644 --- a/pathlib/touch.go +++ b/pathlib/touch.go @@ -8,3 +8,10 @@ import ( func TouchWhen(location string, when time.Time) { os.Chtimes(location, when, when) } + +func ForceTouchWhen(location string, when time.Time) { + if !Exists(location) { + os.WriteFile(location, []byte{}, 0o644) + } + TouchWhen(location, when) +} From 0a135bf16a0e82f5eb6792ee74b222966846d8c1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 31 Aug 2022 11:56:29 +0300 Subject: [PATCH 288/516] FEATURE: catalog usage improvements (v11.22.0) - new command `rcc holotree remove` added, and this will remove catalogs from holotree library (hololib) - added repeat count to holotree check command (used also from remove command) --- cmd/holotreeCheck.go | 54 ++++++++++++++++++++++++++++++++----------- cmd/holotreeDelete.go | 7 +++--- cmd/holotreeRemove.go | 50 +++++++++++++++++++++++++++++++++++++++ common/exit.go | 6 ++++- common/version.go | 2 +- docs/changelog.md | 6 +++++ fail/handling.go | 10 ++++++-- htfs/library.go | 19 +++++++++++++++ htfs/virtual.go | 4 ++++ 9 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 cmd/holotreeRemove.go diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index f2d9b6dd..4ff17f50 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -6,28 +6,35 @@ import ( "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) -func checkHolotreeIntegrity() { +var ( + checkRetries int +) + +func checkHolotreeIntegrity() (err error) { + defer fail.Around(&err) + common.Timeline("holotree integrity check start") defer common.Timeline("holotree integrity check done") fs, err := htfs.NewRoot(common.HololibLibraryLocation()) - pretty.Guard(err == nil, 1, "%s", err) + fail.On(err != nil, "%s", err) common.Timeline("holotree integrity lift") err = fs.Lift() - pretty.Guard(err == nil, 2, "%s", err) + fail.On(err != nil, "%s", err) common.Timeline("holotree integrity hasher") known, needed := htfs.LoadHololibHashes() err = fs.AllFiles(htfs.Hasher(known)) - pretty.Guard(err == nil, 3, "%s", err) + fail.On(err != nil, "%s", err) collector := make(map[string]string) common.Timeline("holotree integrity collector") err = fs.Treetop(htfs.IntegrityCheck(collector, needed)) common.Timeline("holotree integrity report") - pretty.Guard(err == nil, 4, "%s", err) + fail.On(err != nil, "%s", err) purge := make(map[string]bool) for k, _ := range collector { found, ok := known[filepath.Base(k)] @@ -49,24 +56,43 @@ func checkHolotreeIntegrity() { redo = true anywork.Backlog(htfs.RemoveFile(k)) } - if redo { - pretty.Warning("Some catalogs were purged. Run this check command again, please!") - } err = anywork.Sync() - pretty.Guard(err == nil, 5, "%s", err) - pretty.Guard(len(collector) == 0, 6, "Size: %d", len(collector)) + fail.On(err != nil, "%s", err) + fail.On(redo, "Some catalogs were purged. Run this check command again, please!") + fail.On(len(collector) > 0, "Size: %d", len(collector)) + return nil +} + +func checkLoop(retryCount int) { + var err error +loop: + for retryCount > 0 { + retryCount-- + err = checkHolotreeIntegrity() + if err == nil { + break loop + } + common.Timeline("!!! holotree integrity retry needed [remaining: %d]", retryCount) + } + pretty.Guard(err == nil, 1, "%s", err) } var holotreeCheckCmd = &cobra.Command{ - Use: "check", - Short: "Check holotree library integrity.", - Long: "Check holotree library integrity.", + Use: "check", + Short: "Check holotree library integrity.", + Long: "Check holotree library integrity.", + Aliases: []string{"chk"}, Run: func(cmd *cobra.Command, args []string) { - checkHolotreeIntegrity() + repeat := 1 + if checkRetries > 0 { + repeat += checkRetries + } + checkLoop(repeat) pretty.Ok() }, } func init() { + holotreeCheckCmd.Flags().IntVarP(&checkRetries, "retries", "r", 1, "How many retries to do in case of failures.") holotreeCmd.AddCommand(holotreeCheckCmd) } diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go index d09b335f..ebd5e5c0 100644 --- a/cmd/holotreeDelete.go +++ b/cmd/holotreeDelete.go @@ -22,9 +22,10 @@ func deleteByPartialIdentity(partials []string) { } var holotreeDeleteCmd = &cobra.Command{ - Use: "delete *", - Short: "Delete holotree controller space.", - Long: "Delete holotree controller space.", + Use: "delete *", + Short: "Delete holotree controller space.", + Long: "Delete holotree controller space.", + Aliases: []string{"del"}, Run: func(cmd *cobra.Command, args []string) { partials := make([]string, 0, len(args)+1) if len(args) > 0 { diff --git a/cmd/holotreeRemove.go b/cmd/holotreeRemove.go new file mode 100644 index 00000000..75f54dc3 --- /dev/null +++ b/cmd/holotreeRemove.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + removeCheckRetries int +) + +func holotreeRemove(catalogs []string) { + common.Debug("Trying to remove following catalogs:") + for _, catalog := range catalogs { + common.Debug("- %s", catalog) + } + + tree, err := htfs.New() + pretty.Guard(err == nil, 2, "%s", err) + + err = tree.Remove(catalogs) + pretty.Guard(err == nil, 3, "%s", err) +} + +var holotreeRemoveCmd = &cobra.Command{ + Use: "remove catalog+", + Short: "Remove existing holotree catalogs.", + Long: "Remove existing holotree catalogs. Partial identities are ok.", + Aliases: []string{"rm"}, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree remove command lasted").Report() + } + if removeCheckRetries > 0 { + checkLoop(removeCheckRetries) + } else { + pretty.Warning("Remember to run `rcc holotree check` after you have removed all desired catalogs!") + } + holotreeRemove(selectCatalogs(args)) + pretty.Ok() + }, +} + +func init() { + holotreeRemoveCmd.Flags().IntVarP(&removeCheckRetries, "check", "c", 0, "Additionally run holotree check with this many times.") + holotreeCmd.AddCommand(holotreeRemoveCmd) +} diff --git a/common/exit.go b/common/exit.go index 30e68071..bf10c439 100644 --- a/common/exit.go +++ b/common/exit.go @@ -14,8 +14,12 @@ func (it ExitCode) ShowMessage() { } func Exit(code int, format string, rest ...interface{}) { + message := format + if len(rest) > 0 { + message = fmt.Sprintf(format, rest...) + } panic(ExitCode{ Code: code, - Message: fmt.Sprintf(format, rest...), + Message: message, }) } diff --git a/common/version.go b/common/version.go index 54b828c2..bcbdb817 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.21.0` + Version = `v11.22.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5ec5385d..fb534a47 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.22.0 (date: 31.8.2022) + +- new command `rcc holotree remove` added, and this will remove catalogs + from holotree library (hololib) +- added repeat count to holotree check command (used also from remove command) + ## v11.21.0 (date: 29.8.2022) - added support to tracking when catalog blueprints are used diff --git a/fail/handling.go b/fail/handling.go index 09a460e2..f77da952 100644 --- a/fail/handling.go +++ b/fail/handling.go @@ -1,6 +1,9 @@ package fail -import "fmt" +import ( + "errors" + "fmt" +) func Around(err *error) { original := recover() @@ -23,7 +26,10 @@ func On(condition bool, form string, details ...interface{}) { } func failure(form string, details ...interface{}) delimited { - err := fmt.Errorf(form, details...) + err := errors.New(form) + if len(details) > 0 { + err = fmt.Errorf(form, details...) + } return func() error { return err } diff --git a/htfs/library.go b/htfs/library.go index 783c2eb8..d27fb70d 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -58,6 +58,7 @@ type MutableLibrary interface { Identity() string ExactLocation(string) string Export([]string, string) error + Remove([]string) error Location(string) string Record([]byte) error Stage() string @@ -118,6 +119,24 @@ func (it zipseen) Add(fullpath, relativepath string) (err error) { return nil } +func (it *hololib) Remove(catalogs []string) (err error) { + defer fail.Around(&err) + + common.TimelineBegin("holotree remove start") + defer common.TimelineEnd() + + for _, name := range catalogs { + catalog := filepath.Join(common.HololibCatalogLocation(), name) + if !pathlib.IsFile(catalog) { + pretty.Warning("Catalog %s (%s) is not a file! Ignored!", name, catalog) + continue + } + err := os.Remove(catalog) + fail.On(err != nil, "Could not remove catalog %s [filename: %q]", name, catalog) + } + return nil +} + func (it *hololib) Export(catalogs []string, archive string) (err error) { defer fail.Around(&err) diff --git a/htfs/virtual.go b/htfs/virtual.go index 6fdb0c3d..a50a6405 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -39,6 +39,10 @@ func (it *virtual) Stage() string { return stage } +func (it *virtual) Remove([]string) error { + return fmt.Errorf("Not supported yet on virtual holotree.") +} + func (it *virtual) Export([]string, string) error { return fmt.Errorf("Not supported yet on virtual holotree.") } From f87f2d483d56236668657ddfa75969ea1d007267 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 1 Sep 2022 10:13:16 +0300 Subject: [PATCH 289/516] BUGFIX: various bugs fixed (v11.22.1) - fix: using wrong file for age calculation on holotree catalogs - fix: holotree check failed to recover on corrupted files; now failure leads to removal of broken file - fix: empty hololib directories are now removed on holotree check --- cmd/holotreeCatalogs.go | 5 ++--- cmd/holotreeCheck.go | 11 ++++++++++- cmd/holotreeRemove.go | 2 +- common/version.go | 2 +- docs/changelog.md | 9 ++++++++- htfs/directory.go | 7 +++++++ htfs/functions.go | 12 +++++++----- htfs/library.go | 1 + htfs/virtual.go | 4 ++++ pathlib/functions.go | 11 +++++++++++ pathlib/walk.go | 30 ++++++++++++++++++++++++++++++ 11 files changed, 82 insertions(+), 12 deletions(-) diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index 8ba86e5f..e6d7440b 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -66,7 +66,7 @@ func jsonCatalogDetails(roots []*htfs.Root) { data["files"] = stats.Files data["bytes"] = stats.Bytes holder[catalog.Blueprint] = data - age, _ := pathlib.DaysSinceModified(identity) + age, _ := pathlib.DaysSinceModified(catalog.Source()) data["age_in_days"] = age data["days_since_last_use"] = lastUse } @@ -88,8 +88,7 @@ func listCatalogDetails(roots []*htfs.Root) { } stats, err := catalog.Stats() pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) - identity := filepath.Join(common.HololibLibraryLocation(), stats.Identity) - days, _ := pathlib.DaysSinceModified(identity) + days, _ := pathlib.DaysSinceModified(catalog.Source()) data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\t%10d\t%11d\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase(), days, lastUse) tabbed.Write([]byte(data)) } diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index 4ff17f50..4f994c66 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -2,12 +2,14 @@ package cmd import ( "fmt" + "os" "path/filepath" "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -28,7 +30,7 @@ func checkHolotreeIntegrity() (err error) { fail.On(err != nil, "%s", err) common.Timeline("holotree integrity hasher") known, needed := htfs.LoadHololibHashes() - err = fs.AllFiles(htfs.Hasher(known)) + err = fs.AllFiles(htfs.CheckHasher(known)) fail.On(err != nil, "%s", err) collector := make(map[string]string) common.Timeline("holotree integrity collector") @@ -60,6 +62,13 @@ func checkHolotreeIntegrity() (err error) { fail.On(err != nil, "%s", err) fail.On(redo, "Some catalogs were purged. Run this check command again, please!") fail.On(len(collector) > 0, "Size: %d", len(collector)) + err = pathlib.DirWalk(common.HololibLibraryLocation(), func(fullpath, relative string, entry os.FileInfo) { + if pathlib.IsEmptyDir(fullpath) { + err = os.Remove(fullpath) + fail.On(err != nil, "%s", err) + } + }) + fail.On(err != nil, "%s", err) return nil } diff --git a/cmd/holotreeRemove.go b/cmd/holotreeRemove.go index 75f54dc3..6e82eef6 100644 --- a/cmd/holotreeRemove.go +++ b/cmd/holotreeRemove.go @@ -34,12 +34,12 @@ var holotreeRemoveCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree remove command lasted").Report() } + holotreeRemove(selectCatalogs(args)) if removeCheckRetries > 0 { checkLoop(removeCheckRetries) } else { pretty.Warning("Remember to run `rcc holotree check` after you have removed all desired catalogs!") } - holotreeRemove(selectCatalogs(args)) pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index bcbdb817..b848c0ad 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.22.0` + Version = `v11.22.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index fb534a47..a856db4f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,12 +1,19 @@ # rcc change log +## v11.22.1 (date: 1.9.2022) + +- fix: using wrong file for age calculation on holotree catalogs +- fix: holotree check failed to recover on corrupted files; now failure + leads to removal of broken file +- fix: empty hololib directories are now removed on holotree check + ## v11.22.0 (date: 31.8.2022) - new command `rcc holotree remove` added, and this will remove catalogs from holotree library (hololib) - added repeat count to holotree check command (used also from remove command) -## v11.21.0 (date: 29.8.2022) +## v11.21.0 (date: 30.8.2022) - added support to tracking when catalog blueprints are used - if there is no tracking info on existing catalog, first reporting will diff --git a/htfs/directory.go b/htfs/directory.go index 89e3eb42..24ecf2c5 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -49,6 +49,7 @@ type Root struct { Blueprint string `json:"blueprint"` Lifted bool `json:"lifted"` Tree *Dir `json:"tree"` + source string } func NewRoot(path string) (*Root, error) { @@ -64,9 +65,14 @@ func NewRoot(path string) (*Root, error) { Lifted: false, Tree: newDir("", "", false), RccVersion: common.Version, + source: fullpath, }, nil } +func (it *Root) Source() string { + return it.source +} + func (it *Root) Touch() { touchUsedHash(it.Blueprint) } @@ -185,6 +191,7 @@ func (it *Root) LoadFrom(filename string) error { if err != nil { return err } + it.source = filename defer reader.Close() return it.ReadFrom(reader) } diff --git a/htfs/functions.go b/htfs/functions.go index 31f245f2..e967e575 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -93,7 +93,7 @@ func IntegrityCheck(result map[string]string, needed map[string]map[string]bool) return tool } -func Hasher(known map[string]map[string]bool) Filetask { +func CheckHasher(known map[string]map[string]bool) Filetask { return func(fullpath string, details *File) anywork.Work { return func() { _, ok := known[details.Name] @@ -102,7 +102,8 @@ func Hasher(known map[string]map[string]bool) Filetask { } source, err := os.Open(fullpath) if err != nil { - panic(fmt.Sprintf("Open %q, reason: %v", fullpath, err)) + anywork.Backlog(RemoveFile(fullpath)) + panic(fmt.Sprintf("Open[check] %q, reason: %v", fullpath, err)) } defer source.Close() @@ -116,7 +117,8 @@ func Hasher(known map[string]map[string]bool) Filetask { digest := sha256.New() _, err = io.Copy(digest, reader) if err != nil { - panic(fmt.Sprintf("Copy %q, reason: %v", fullpath, err)) + anywork.Backlog(RemoveFile(fullpath)) + panic(fmt.Sprintf("Copy[check] %q, reason: %v", fullpath, err)) } details.Digest = fmt.Sprintf("%02x", digest.Sum(nil)) } @@ -128,14 +130,14 @@ func Locator(seek string) Filetask { return func() { source, err := os.Open(fullpath) if err != nil { - panic(fmt.Sprintf("Open %q, reason: %v", fullpath, err)) + panic(fmt.Sprintf("Open[Locator] %q, reason: %v", fullpath, err)) } defer source.Close() digest := sha256.New() locator := trollhash.LocateWriter(digest, seek) _, err = io.Copy(locator, source) if err != nil { - panic(fmt.Sprintf("Copy %q, reason: %v", fullpath, err)) + panic(fmt.Sprintf("Copy[Locator] %q, reason: %v", fullpath, err)) } details.Rewrite = locator.Locations() details.Digest = fmt.Sprintf("%02x", digest.Sum(nil)) diff --git a/htfs/library.go b/htfs/library.go index d27fb70d..5582e49a 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -62,6 +62,7 @@ type MutableLibrary interface { Location(string) string Record([]byte) error Stage() string + CatalogPath(string) string } type hololib struct { diff --git a/htfs/virtual.go b/htfs/virtual.go index a50a6405..474662e9 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -39,6 +39,10 @@ func (it *virtual) Stage() string { return stage } +func (it *virtual) CatalogPath(key string) string { + return "Virtual Does Not Support Catalog Path Request" +} + func (it *virtual) Remove([]string) error { return fmt.Errorf("Not supported yet on virtual holotree.") } diff --git a/pathlib/functions.go b/pathlib/functions.go index 9188e2af..b4739102 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -45,6 +45,17 @@ func IsDir(pathname string) bool { return err == nil && stat.IsDir() } +func IsEmptyDir(pathname string) bool { + if !IsDir(pathname) { + return false + } + content, err := os.ReadDir(pathname) + if err != nil { + return false + } + return len(content) == 0 +} + func IsFile(pathname string) bool { stat, err := os.Stat(pathname) return err == nil && !stat.IsDir() diff --git a/pathlib/walk.go b/pathlib/walk.go index 6706813c..d009cf27 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -133,6 +133,24 @@ func folderEntries(directory string) ([]os.FileInfo, error) { return entries, nil } +func recursiveDirWalk(here os.FileInfo, directory, prefix string, report Report) error { + entries, err := folderEntries(directory) + if err != nil { + return err + } + sorted(entries) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + nextPrefix := filepath.Join(prefix, entry.Name()) + entryPath := filepath.Join(directory, entry.Name()) + recursiveDirWalk(entry, entryPath, nextPrefix, report) + } + report(directory, prefix, here) + return nil +} + func recursiveWalk(directory, prefix string, force Forced, ignore Ignore, report Report) error { entries, err := folderEntries(directory) if err != nil { @@ -162,6 +180,18 @@ func ForceWalk(directory string, force Forced, ignore Ignore, report Report) err return recursiveWalk(fullpath, ".", force, ignore, report) } +func DirWalk(directory string, report Report) error { + fullpath, err := filepath.Abs(directory) + if err != nil { + return err + } + entry, err := os.Stat(fullpath) + if err != nil { + return err + } + return recursiveDirWalk(entry, fullpath, ".", report) +} + func Walk(directory string, ignore Ignore, report Report) error { return ForceWalk(directory, ForceNothing, ignore, report) } From 04c1acf58041a2e126f8be5cb6f45fc7310a4c62 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 2 Sep 2022 08:47:22 +0300 Subject: [PATCH 290/516] FEATURE: remove catalogs by idle days (v11.23.0) - added unused option to holotree catalog removal command - added maintenance related robot test suite - minor documentation updates --- cmd/holotreeRemove.go | 23 ++++++++++++++-- common/version.go | 2 +- docs/changelog.md | 6 ++++ docs/profile_configuration.md | 3 +- robot_tests/maintenance.robot | 52 +++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 robot_tests/maintenance.robot diff --git a/cmd/holotreeRemove.go b/cmd/holotreeRemove.go index 6e82eef6..89aeca14 100644 --- a/cmd/holotreeRemove.go +++ b/cmd/holotreeRemove.go @@ -9,9 +9,14 @@ import ( var ( removeCheckRetries int + unusedDays int ) func holotreeRemove(catalogs []string) { + if len(catalogs) == 0 { + pretty.Warning("No catalogs given, so nothing to do. Quitting!") + return + } common.Debug("Trying to remove following catalogs:") for _, catalog := range catalogs { common.Debug("- %s", catalog) @@ -24,16 +29,29 @@ func holotreeRemove(catalogs []string) { pretty.Guard(err == nil, 3, "%s", err) } +func allUnusedCatalogs(limit int) []string { + result := []string{} + used := catalogUsedStats() + for name, idle := range used { + if idle > limit { + result = append(result, name) + } + } + return result +} + var holotreeRemoveCmd = &cobra.Command{ - Use: "remove catalog+", + Use: "remove catalogid*", Short: "Remove existing holotree catalogs.", Long: "Remove existing holotree catalogs. Partial identities are ok.", Aliases: []string{"rm"}, - Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Holotree remove command lasted").Report() } + if unusedDays > 0 { + args = append(args, allUnusedCatalogs(unusedDays)...) + } holotreeRemove(selectCatalogs(args)) if removeCheckRetries > 0 { checkLoop(removeCheckRetries) @@ -46,5 +64,6 @@ var holotreeRemoveCmd = &cobra.Command{ func init() { holotreeRemoveCmd.Flags().IntVarP(&removeCheckRetries, "check", "c", 0, "Additionally run holotree check with this many times.") + holotreeRemoveCmd.Flags().IntVarP(&unusedDays, "unused", "", 0, "Remove idle/unused catalog entries based on idle days when value is above given limit.") holotreeCmd.AddCommand(holotreeRemoveCmd) } diff --git a/common/version.go b/common/version.go index b848c0ad..60d3fc0e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.22.1` + Version = `v11.23.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index a856db4f..83208261 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.23.0 (date: 2.9.2022) + +- added unused option to holotree catalog removal command +- added maintenance related robot test suite +- minor documentation updates + ## v11.22.1 (date: 1.9.2022) - fix: using wrong file for age calculation on holotree catalogs diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md index bc47a4ab..993f5f2a 100644 --- a/docs/profile_configuration.md +++ b/docs/profile_configuration.md @@ -40,7 +40,8 @@ rcc configuration diagnostics # when basics work, see if full environment creation works rcc configuration speedtest -# when you want to reset profile to "default" state +# when you want to reset profile to "system default" state +# in practice this means that all settings files removed rcc configuration switch --noprofile # if you want to export profile and deliver to others diff --git a/robot_tests/maintenance.robot b/robot_tests/maintenance.robot new file mode 100644 index 00000000..6643781e --- /dev/null +++ b/robot_tests/maintenance.robot @@ -0,0 +1,52 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot + +*** Test cases *** + +Goal: Can see human readable catalog of robots + Step build/rcc holotree catalogs --controller citests + Use STDERR + Must Have inside hololib + Must Have Age (days) + Must Have Idle (days) + Must Have ffd32af1fdf0f253 + Must Have OK. + +Goal: Can see machine readable catalog of robots + Step build/rcc holotree catalogs --controller citests --json + Must Be Json Response + Must Have ffd32af1fdf0f253 + Must Have "age_in_days": 0, + Must Have "days_since_last_use": 0, + Must Have "holotree": + Must Have "blueprint": + Must Have "files": + Must Have "directories": + +Goal: Can check holotree with retries + Step build/rcc holotree check --retries 5 --controller citests + Use STDERR + Must Have OK. + +Goal: Can remove catalogs with check from hololib by ids and give warnings + Step build/rcc holotree remove cafebabe9000 --check 5 --controller citests + Use STDERR + Must Have Warning: No catalogs given, so nothing to do. Quitting! + Wont Have Warning: Remember to run `rcc holotree check` after you have removed all desired catalogs! + Must Have OK. + +Goal: Can remove catalogs from hololib by idle days and give warnings + Step build/rcc holotree remove --unused 90 --controller citests + Use STDERR + Must Have Warning: No catalogs given, so nothing to do. Quitting! + Must Have Warning: Remember to run `rcc holotree check` after you have removed all desired catalogs! + Must Have OK. + +Goal: Can remove catalogs with check from hololib by ids correctly + Step build/rcc holotree remove ffd32af1fdf0f253 --check 5 --controller citests + Use STDERR + Wont Have Warning: No catalogs given, so nothing to do. Quitting! + Wont Have Warning: Remember to run `rcc holotree check` after you have removed all desired catalogs! + Must Have OK. From fa79e378a481937910bfb158b11b18a1c86345fa Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 2 Sep 2022 13:10:33 +0300 Subject: [PATCH 291/516] FEATURE: minor feature additions (v11.24.0) - refactoring some utility functions to more common locations - adding rcc and micromamba binary locations to diagnostics - added `RCC_EXE` environment variable available for robots - added `RCC_NO_BUILD` environment variable support (in addition to previous settings options and CLI flag; see v11.19.0) - some documentation updates - added support for toplevel `--version` option --- cmd/holotreeCheck.go | 8 +------- cmd/root.go | 11 +++++++++-- common/version.go | 2 +- conda/robocorp.go | 1 + docs/changelog.md | 10 ++++++++++ docs/recipes.md | 3 +++ operations/diagnostics.go | 5 +++-- pathlib/functions.go | 11 +++++++++++ robot_tests/exitcodes.robot | 1 + robot_tests/fullrun.robot | 3 +++ settings/settings.go | 3 ++- 11 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index 4f994c66..285a6cd2 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "path/filepath" "github.com/robocorp/rcc/anywork" @@ -62,12 +61,7 @@ func checkHolotreeIntegrity() (err error) { fail.On(err != nil, "%s", err) fail.On(redo, "Some catalogs were purged. Run this check command again, please!") fail.On(len(collector) > 0, "Size: %d", len(collector)) - err = pathlib.DirWalk(common.HololibLibraryLocation(), func(fullpath, relative string, entry os.FileInfo) { - if pathlib.IsEmptyDir(fullpath) { - err = os.Remove(fullpath) - fail.On(err != nil, "%s", err) - } - }) + err = pathlib.RemoveEmptyDirectores(common.HololibLibraryLocation()) fail.On(err != nil, "%s", err) return nil } diff --git a/cmd/root.go b/cmd/root.go index bb2bcbf6..301c1f52 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,6 +20,7 @@ import ( var ( profilefile string profiling *os.File + versionFlag bool ) func toplevelCommands(parent *cobra.Command) { @@ -59,8 +60,12 @@ var rootCmd = &cobra.Command{ communicating with Robocorp Control Room, and managing virtual environments where tasks can be developed, debugged, and run.`, Run: func(cmd *cobra.Command, args []string) { - commandTree(0, "", cmd.Root()) - toplevelCommands(cmd.Root()) + if versionFlag { + common.Stdout("%s\n", common.Version) + } else { + commandTree(0, "", cmd.Root()) + toplevelCommands(cmd.Root()) + } }, } @@ -93,6 +98,8 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) + rootCmd.Flags().BoolVarP(&versionFlag, "version", "v", false, "Show rcc version and exit.") + rootCmd.PersistentFlags().StringVar(&profilefile, "pprof", "", "Filename to save profiling information.") rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") diff --git a/common/version.go b/common/version.go index 60d3fc0e..b8ff5bb5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.23.0` + Version = `v11.24.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 4f4e0f21..f9348fe5 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -122,6 +122,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), + "RCC_EXE="+common.BinRcc(), "RCC_VERSION="+common.Version, "TEMP="+common.RobocorpTemp(), "TMP="+common.RobocorpTemp(), diff --git a/docs/changelog.md b/docs/changelog.md index 83208261..90fb4102 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # rcc change log +## v11.24.0 (date: 2.9.2022) + +- refactoring some utility functions to more common locations +- adding rcc and micromamba binary locations to diagnostics +- added `RCC_EXE` environment variable available for robots +- added `RCC_NO_BUILD` environment variable support (in addition to + previous settings options and CLI flag; see v11.19.0) +- some documentation updates +- added support for toplevel `--version` option + ## v11.23.0 (date: 2.9.2022) - added unused option to holotree catalog removal command diff --git a/docs/recipes.md b/docs/recipes.md index c84d36cd..36d69de6 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -416,6 +416,9 @@ rcc holotree init --revoke so that failing environment creation can be seen with more details - `RCC_CREDENTIALS_ID` is way to provide Control Room credentials using environment variables +- `RCC_NO_BUILD` with any non-empty value will prevent rcc for creating + new environments (also available as `--no-build` CLI flag, and as + an option in `settings.yaml` file) ## How to troubleshoot rcc setup and robots? diff --git a/operations/diagnostics.go b/operations/diagnostics.go index e818c6a8..8e72dc87 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -61,11 +61,12 @@ func RunDiagnostics() *common.DiagnosticStatus { Details: make(map[string]string), Checks: []*common.DiagnosticCheck{}, } - executable, _ := os.Executable() - result.Details["executable"] = executable + result.Details["executable"] = common.BinRcc() result.Details["rcc"] = common.Version + result.Details["rcc.bin"] = common.BinRcc() result.Details["stats"] = rccStatusLine() result.Details["micromamba"] = conda.MicromambaVersion() + result.Details["micromamba.bin"] = conda.BinMicromamba() result.Details["ROBOCORP_HOME"] = common.RobocorpHome() result.Details["ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS"] = fmt.Sprintf("%v", common.OverrideSystemRequirements()) result.Details["RCC_VERBOSE_ENVIRONMENT_BUILDING"] = fmt.Sprintf("%v", common.VerboseEnvironmentBuilding()) diff --git a/pathlib/functions.go b/pathlib/functions.go index b4739102..8dd527b2 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -173,3 +173,14 @@ func EnsureDirectory(directory string) (string, error) { func EnsureParentDirectory(resource string) (string, error) { return EnsureDirectory(filepath.Dir(resource)) } + +func RemoveEmptyDirectores(starting string) (err error) { + defer fail.Around(&err) + + return DirWalk(starting, func(fullpath, relative string, entry os.FileInfo) { + if IsEmptyDir(fullpath) { + err = os.Remove(fullpath) + fail.On(err != nil, "%s", err) + } + }) +} diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index 8666da92..79e8cb0a 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -36,6 +36,7 @@ Run rcc docs tutorial 0 build/rcc docs tutorial --controller c Run rcc holotree list 0 build/rcc holotree list --controller citests Run rcc tutorial 0 build/rcc tutorial --controller citests Run rcc version 0 build/rcc version --controller citests +Run rcc --version 0 build/rcc --version --controller citests *** Keywords *** diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 5b990a78..6882a749 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -144,6 +144,7 @@ Goal: See variables from specific environment without robot.yaml knowledge Step build/rcc holotree variables --controller citests conda/testdata/conda.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= + Must Have RCC_EXE= Must Have CONDA_DEFAULT_ENV=rcc Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) @@ -167,6 +168,7 @@ Goal: See variables from specific environment with robot.yaml but without task Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= + Must Have RCC_EXE= Must Have CONDA_DEFAULT_ENV=rcc Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) @@ -193,6 +195,7 @@ Goal: See variables from specific environment with robot.yaml knowledge Step build/rcc holotree variables --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= + Must Have RCC_EXE= Must Have CONDA_DEFAULT_ENV=rcc Must Have CONDA_PREFIX= Must Have CONDA_PROMPT_MODIFIER=(rcc) diff --git a/settings/settings.go b/settings/settings.go index 4887e77b..dd9c4e65 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -241,7 +241,8 @@ func (it gateway) NoRevocation() bool { } func (it gateway) NoBuild() bool { - return common.NoBuild || it.Option("no-build") + nobuild := len(os.Getenv("RCC_NO_BUILD")) > 0 + return nobuild || common.NoBuild || it.Option("no-build") } func (it gateway) ConfiguredHttpTransport() *http.Transport { From 476878e09263d7659471d77e6b0aff4cbb0e0e86 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 5 Sep 2022 14:24:27 +0300 Subject: [PATCH 292/516] FEATURE: show identity.yaml (v11.25.0) - flag to show identity.yaml (conda.yaml) in holotree catalogs listing and functionality then just show it as part of output, both human readable and machine readable (JSON) --- cmd/holotreeCatalogs.go | 31 +++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/delegates.go | 7 +++++-- htfs/directory.go | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index e6d7440b..c17e3fca 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "text/tabwriter" "github.com/robocorp/rcc/common" @@ -14,6 +15,10 @@ import ( "github.com/spf13/cobra" ) +var ( + showIdentityYaml bool +) + const mega = 1024 * 1024 func megas(bytes uint64) uint64 { @@ -45,6 +50,23 @@ func catalogUsedStats() map[string]int { return result } +func identityContent(catalog *htfs.Root) string { + blob, err := catalog.Show("identity.yaml") + if err != nil { + return err.Error() + } + return string(blob) +} + +func identityContentLines(catalog *htfs.Root) []string { + content := identityContent(catalog) + result := strings.SplitAfter(content, "\n") + for at, value := range result { + result[at] = strings.Replace(strings.TrimRight(value, "\r\n\t "), "\t", " ", -1) + } + return result +} + func jsonCatalogDetails(roots []*htfs.Root) { used := catalogUsedStats() holder := make(map[string]map[string]interface{}) @@ -61,6 +83,9 @@ func jsonCatalogDetails(roots []*htfs.Root) { data["holotree"] = catalog.HolotreeBase() identity := filepath.Join(common.HololibLibraryLocation(), stats.Identity) data["identity.yaml"] = identity + if showIdentityYaml { + data["identity-content"] = identityContent(catalog) + } data["platform"] = catalog.Platform data["directories"] = stats.Directories data["files"] = stats.Files @@ -91,6 +116,11 @@ func listCatalogDetails(roots []*htfs.Root) { days, _ := pathlib.DaysSinceModified(catalog.Source()) data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\t%10d\t%11d\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase(), days, lastUse) tabbed.Write([]byte(data)) + if showIdentityYaml { + for _, line := range identityContentLines(catalog) { + tabbed.Write([]byte(fmt.Sprintf("\t\t\t\t\t%s\n", line))) + } + } } tabbed.Flush() } @@ -116,4 +146,5 @@ var holotreeCatalogsCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeCatalogsCmd) holotreeCatalogsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + holotreeCatalogsCmd.Flags().BoolVarP(&showIdentityYaml, "identity", "i", false, "Show identity.yaml in catalog context.") } diff --git a/common/version.go b/common/version.go index b8ff5bb5..c1ccb2fc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.24.0` + Version = `v11.25.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 90fb4102..d76a260e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.25.0 (date: 5.9.2022) + +- flag to show identity.yaml (conda.yaml) in holotree catalogs listing + and functionality then just show it as part of output, both human readable + and machine readable (JSON) + ## v11.24.0 (date: 2.9.2022) - refactoring some utility functions to more common locations diff --git a/htfs/delegates.go b/htfs/delegates.go index 3a6eb3f8..d61935f0 100644 --- a/htfs/delegates.go +++ b/htfs/delegates.go @@ -8,10 +8,9 @@ import ( "github.com/robocorp/rcc/fail" ) -func delegateOpen(it MutableLibrary, digest string, ungzip bool) (readable io.Reader, closer Closer, err error) { +func gzDelegateOpen(filename string, ungzip bool) (readable io.Reader, closer Closer, err error) { defer fail.Around(&err) - filename := it.ExactLocation(digest) source, err := os.Open(filename) fail.On(err != nil, "Failed to open %q -> %v", filename, err) @@ -28,3 +27,7 @@ func delegateOpen(it MutableLibrary, digest string, ungzip bool) (readable io.Re } return reader, closer, nil } + +func delegateOpen(it MutableLibrary, digest string, ungzip bool) (readable io.Reader, closer Closer, err error) { + return gzDelegateOpen(it.ExactLocation(digest), ungzip) +} diff --git a/htfs/directory.go b/htfs/directory.go index 24ecf2c5..7d936f94 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -1,6 +1,7 @@ package htfs import ( + "bytes" "compress/gzip" "encoding/json" "fmt" @@ -12,6 +13,7 @@ import ( "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" ) @@ -69,6 +71,10 @@ func NewRoot(path string) (*Root, error) { }, nil } +func (it *Root) Show(filename string) ([]byte, error) { + return it.Tree.Show(filepath.SplitList(filename), filename) +} + func (it *Root) Source() string { return it.source } @@ -205,6 +211,36 @@ type Dir struct { Shadow bool `json:"shadow,omitempty"` } +func showFile(filename string) (content []byte, err error) { + defer fail.Around(&err) + + reader, closer, err := gzDelegateOpen(filename, true) + fail.On(err != nil, "Failed to open %q, reason: %v", filename, err) + defer closer() + + sink := bytes.NewBuffer(nil) + _, err = io.Copy(sink, reader) + fail.On(err != nil, "Failed to read %q, reason: %v", filename, err) + return sink.Bytes(), nil +} + +func (it *Dir) Show(path []string, fullpath string) ([]byte, error) { + if len(path) > 1 { + subtree, ok := it.Dirs[path[0]] + if !ok { + return nil, fmt.Errorf("Not found: %s", fullpath) + } + return subtree.Show(path[1:], fullpath) + } + file, ok := it.Files[path[0]] + if !ok { + return nil, fmt.Errorf("Not found: %s", fullpath) + } + location := guessLocation(file.Digest) + rawfile := filepath.Join(common.HololibLibraryLocation(), location) + return showFile(rawfile) +} + func (it *Dir) IsSymlink() bool { return len(it.Symlink) > 0 } From 70299bacecbadb856ef47b0f71b1afd779a38c56 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 6 Sep 2022 07:07:23 +0300 Subject: [PATCH 293/516] BUGFIX: symlink restore (v11.25.1) - fix: symbolic link restoration, when target is actually non-symlink --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/functions.go | 25 ++++++++++++++++++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index c1ccb2fc..ec08e4d7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.25.0` + Version = `v11.25.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index d76a260e..4f3c6ff5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.25.1 (date: 6.9.2022) + +- fix: symbolic link restoration, when target is actually non-symlink + ## v11.25.0 (date: 5.9.2022) - flag to show identity.yaml (conda.yaml) in holotree catalogs listing diff --git a/htfs/functions.go b/htfs/functions.go index e967e575..8d8a771b 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -146,10 +146,12 @@ func Locator(seek string) Filetask { } func MakeBranches(path string, it *Dir) error { - if it.IsSymlink() { - anywork.OnErrPanicCloseAll(restoreSymlink(it.Symlink, path)) + if it.Shadow || it.IsSymlink() { return nil } + if _, ok := pathlib.Symlink(path); ok { + os.Remove(path) + } hasSymlinks := false detector: for _, subdir := range it.Dirs { @@ -382,12 +384,16 @@ func CalculateTreeStats() (Dirtask, *TreeStats) { }, result } -func restoreSymlink(source, target string) error { +func isCorrectSymlink(source, target string) bool { old, ok := pathlib.Symlink(target) - if ok && old == target { + return ok && old == source +} + +func restoreSymlink(source, target string) error { + if isCorrectSymlink(source, target) { return nil } - os.Remove(target) + os.RemoveAll(target) return os.Symlink(source, target) } @@ -415,6 +421,11 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat stats.Dirty(!ok) continue } + link, ok := it.Dirs[part.Name()] + if ok && link.IsSymlink() { + stats.Dirty(false) + continue + } files[part.Name()] = true found, ok := it.Files[part.Name()] if !ok { @@ -423,6 +434,10 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat stats.Dirty(true) continue } + if found.IsSymlink() && isCorrectSymlink(found.Symlink, directpath) { + stats.Dirty(false) + continue + } shadow, ok := current[directpath] golden := !ok || found.Digest == shadow info, err := part.Info() From 237ff83341c501734016e51430721b9cd28d0d75 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Sep 2022 09:24:31 +0300 Subject: [PATCH 294/516] EXPERIMENT: pyvenv.cfg generation (v11.26.0) - experiment: pyvenv.cfg file written into created holotree before lifting - update: cloud-linking in setting.yaml now points to new default location: https://cloud.robocorp.com/link/ - bugfix: settings.yaml version updated to 2022.09 (because options section) --- assets/settings.yaml | 4 ++-- common/version.go | 2 +- conda/workflows.go | 25 +++++++++++++++++++++++++ docs/changelog.md | 7 +++++++ settings/settings_test.go | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index cbe8b895..a53971e8 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -1,6 +1,6 @@ endpoints: cloud-api: https://api.eu1.robocorp.com/ - cloud-linking: https://id.robocorp.com/ + cloud-linking: https://cloud.robocorp.com/link/ cloud-ui: https://cloud.robocorp.com/ pypi: # https://pypi.org/simple/ pypi-trusted: # https://pypi.org/ @@ -42,4 +42,4 @@ meta: name: default description: default settings.yaml internal to rcc source: builtin - version: 2022.03 + version: 2022.09 diff --git a/common/version.go b/common/version.go index ec08e4d7..2109e7cf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.25.1` + Version = `v11.26.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index dd3e4780..2e7c7c95 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -20,6 +20,13 @@ import ( "github.com/robocorp/rcc/xviper" ) +const ( + venvTemplate = `home = %s +include-system-site-packages = true +version = %s +` +) + func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } @@ -247,9 +254,27 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } + _, ok = HolotreePath(targetFolder).Which("python", FileExtensions) + if ok { + venvContent := fmt.Sprintf(venvTemplate, targetFolder, pythonVersionAt(targetFolder)) + venvFile := filepath.Join(targetFolder, "pyvenv.cfg") + err = ioutil.WriteFile(venvFile, []byte(venvContent), 0o644) + if err != nil { + return false, false + } + } + return true, false } +func pythonVersionAt(targetFolder string) string { + versionText, code, _ := LiveCapture(targetFolder, "python", "--version") + if code != 0 { + return "?.?.?" + } + return strings.Replace(versionText, "Python ", "", 1) +} + func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { var left, right *Environment var err error diff --git a/docs/changelog.md b/docs/changelog.md index 4f3c6ff5..dc0a3e20 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.26.0 (date: 6.9.2022) + +- experiment: pyvenv.cfg file written into created holotree before lifting +- update: cloud-linking in setting.yaml now points to new default location: + https://cloud.robocorp.com/link/ +- bugfix: settings.yaml version updated to 2022.09 (because options section) + ## v11.25.1 (date: 6.9.2022) - fix: symbolic link restoration, when target is actually non-symlink diff --git a/settings/settings_test.go b/settings/settings_test.go index d3e77c1b..f1cd322d 100644 --- a/settings/settings_test.go +++ b/settings/settings_test.go @@ -35,5 +35,5 @@ func TestThatSomeDefaultValuesAreVisible(t *testing.T) { must_be.Equal("", settings.Global.CondaURL()) must_be.Equal("", settings.Global.HttpProxy()) must_be.Equal("", settings.Global.HttpsProxy()) - must_be.Equal(10, len(settings.Global.Hostnames())) + must_be.Equal(9, len(settings.Global.Hostnames())) } From 1cfee6058da851e8ac4bc2a3149ee12bcd2d3321 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Sep 2022 11:24:11 +0300 Subject: [PATCH 295/516] DOCUMENTATION: configuration settings help improved (v11.26.1) - minor documentation improvement, highlighting configuration settings help, that plain commands are showing vanilla rcc setting by default. --- cmd/settings.go | 7 ++++--- common/version.go | 2 +- docs/changelog.md | 7 ++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/settings.go b/cmd/settings.go index d3414f6e..2eade0bd 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -11,8 +11,9 @@ import ( var settingsCmd = &cobra.Command{ Use: "settings", - Short: "Show default settings.yaml content.", - Long: "Show default settings.yaml content.", + Short: "Show DEFAULT settings.yaml content. Vanilla rcc settings.", + Long: `Show DEFAULT settings.yaml content. Vanilla rcc settings. +If you need active status, either use --json option, or "rcc configuration diagnostics".`, Run: func(cmd *cobra.Command, args []string) { if jsonFlag { config, err := settings.SummonSettings() @@ -30,5 +31,5 @@ var settingsCmd = &cobra.Command{ func init() { configureCmd.AddCommand(settingsCmd) - settingsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show effective settings as JSON stream.") + settingsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show EFFECTIVE settings as JSON stream. For applications to use.") } diff --git a/common/version.go b/common/version.go index 2109e7cf..a2885e73 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.0` + Version = `v11.26.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index dc0a3e20..e161d184 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # rcc change log -## v11.26.0 (date: 6.9.2022) +## v11.26.1 (date: 7.9.2022) + +- minor documentation improvement, highlighting configuration settings help, + that plain commands are showing vanilla rcc setting by default. + +## v11.26.0 (date: 7.9.2022) - experiment: pyvenv.cfg file written into created holotree before lifting - update: cloud-linking in setting.yaml now points to new default location: From 77fdd146c75afd89b7edc38beb839ff51c7ac15b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 8 Sep 2022 10:37:05 +0300 Subject: [PATCH 296/516] REFACTORING: using embed (v11.26.2) - converted assets to embedded resources (golang builtin embed module) - go-bindata is not used anymore (replaced by "embed") --- Rakefile | 17 ++++++++++++----- blobs/.gitignore | 1 - blobs/asset_test.go | 24 ++++++++++++++++++++++-- blobs/assets/.gitignore | 2 ++ blobs/assets/man/.gitignore | 1 + blobs/docs/.gitignore | 1 + blobs/embedded.go | 21 +++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ go.mod | 26 +++++++++++++++++--------- go.sum | 3 --- 11 files changed, 82 insertions(+), 21 deletions(-) delete mode 100644 blobs/.gitignore create mode 100644 blobs/assets/.gitignore create mode 100644 blobs/assets/man/.gitignore create mode 100644 blobs/docs/.gitignore create mode 100644 blobs/embedded.go diff --git a/Rakefile b/Rakefile index 39bd361d..96d163b5 100644 --- a/Rakefile +++ b/Rakefile @@ -16,21 +16,28 @@ task :tooling do puts "PATH is #{ENV['PATH']}" puts "GOPATH is #{ENV['GOPATH']}" puts "GOROOT is #{ENV['GOROOT']}" - sh "go install github.com/go-bindata/go-bindata/..." sh "which -a zip || echo NA" - sh "which -a go-bindata || echo NA" sh "ls -l $HOME/go/bin" end -task :assets do +task :noassets do + rm_f FileList['blobs/assets/*.zip'] + rm_f FileList['blobs/assets/*.yaml'] + rm_f FileList['blobs/assets/man/*.txt'] + rm_f FileList['blobs/docs/*.md'] +end + +task :assets => [:noassets] do FileList['templates/*/'].each do |directory| basename = File.basename(directory) - assetname = File.absolute_path(File.join("assets", "#{basename}.zip")) + assetname = File.absolute_path(File.join("blobs", "assets", "#{basename}.zip")) rm_rf assetname puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/*.md" + cp FileList['assets/*.yaml'], 'blobs/assets/' + cp FileList['assets/man/*.txt'], 'blobs/assets/man/' + cp FileList['docs/*.md'], 'blobs/docs/' end task :clean do diff --git a/blobs/.gitignore b/blobs/.gitignore deleted file mode 100644 index 60ce659f..00000000 --- a/blobs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -assets.go diff --git a/blobs/asset_test.go b/blobs/asset_test.go index f243a195..5f961dda 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -12,10 +12,8 @@ func TestCanSeeBaseZipAsset(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) must_be.Panic(func() { blobs.MustAsset("assets/missing.zip") }) - wont_be.Panic(func() { blobs.MustAsset("assets/templates.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/standard.zip") }) wont_be.Panic(func() { blobs.MustAsset("assets/python.zip") }) - wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) _, err := blobs.Asset("assets/missing.zip") wont_be.Nil(err) @@ -25,6 +23,28 @@ func TestCanSeeBaseZipAsset(t *testing.T) { wont_be.Nil(asset) } +func TestCanOtherAssets(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + must_be.Panic(func() { blobs.MustAsset("assets/missing.yaml") }) + + wont_be.Panic(func() { blobs.MustAsset("assets/templates.yaml") }) + wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) + wont_be.Panic(func() { blobs.MustAsset("assets/speedtest.yaml") }) + + wont_be.Panic(func() { blobs.MustAsset("assets/man/LICENSE.txt") }) + wont_be.Panic(func() { blobs.MustAsset("assets/man/tutorial.txt") }) + + wont_be.Panic(func() { blobs.MustAsset("docs/BUILD.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/README.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/changelog.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/environment-caching.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/features.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/profile_configuration.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/recipes.md") }) + wont_be.Panic(func() { blobs.MustAsset("docs/usecases.md") }) +} + func TestCanGetTemplateNamesThruOperations(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) diff --git a/blobs/assets/.gitignore b/blobs/assets/.gitignore new file mode 100644 index 00000000..c5df735e --- /dev/null +++ b/blobs/assets/.gitignore @@ -0,0 +1,2 @@ +*.zip +*.yaml diff --git a/blobs/assets/man/.gitignore b/blobs/assets/man/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/blobs/assets/man/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/blobs/docs/.gitignore b/blobs/docs/.gitignore new file mode 100644 index 00000000..dd449725 --- /dev/null +++ b/blobs/docs/.gitignore @@ -0,0 +1 @@ +*.md diff --git a/blobs/embedded.go b/blobs/embedded.go new file mode 100644 index 00000000..0a76adaf --- /dev/null +++ b/blobs/embedded.go @@ -0,0 +1,21 @@ +package blobs + +import ( + "embed" +) + +//go:embed assets/*.yaml docs/*.md +//go:embed assets/*.zip assets/man/*.txt +var content embed.FS + +func Asset(name string) ([]byte, error) { + return content.ReadFile(name) +} + +func MustAsset(name string) []byte { + body, err := Asset(name) + if err != nil { + panic(err) + } + return body +} diff --git a/common/version.go b/common/version.go index a2885e73..987d1b9c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.1` + Version = `v11.26.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index e161d184..c40294ac 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.26.2 (date: 8.9.2022) + +- converted assets to embedded resources (golang builtin embed module) +- go-bindata is not used anymore (replaced by "embed") + ## v11.26.1 (date: 7.9.2022) - minor documentation improvement, highlighting configuration settings help, diff --git a/go.mod b/go.mod index 8b763861..fce15ee4 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,32 @@ module github.com/robocorp/rcc -go 1.14 +go 1.18 require ( github.com/dchest/siphash v1.2.2 - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.12 + github.com/spf13/cobra v0.0.7 + github.com/spf13/viper v1.7.1 + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + gopkg.in/square/go-jose.v2 v2.5.1 + gopkg.in/yaml.v2 v2.2.8 +) + +require ( + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/magiconair/properties v1.8.1 // indirect github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/pelletier/go-toml v1.6.0 // indirect github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v0.0.7 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.7.1 - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + github.com/subosito/gotenv v1.2.0 // indirect + golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect + golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.55.0 // indirect - gopkg.in/square/go-jose.v2 v2.5.1 - gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 03f2c30f..3e27b8ca 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= -github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -272,7 +270,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= From 59e8a71d7727e87a2e4a4e50558f04a4afbb9734 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 12 Sep 2022 18:21:51 +0300 Subject: [PATCH 297/516] BUGFIX: ht.lck to ht/global.lck (v11.26.3) - bugfix: moved "ht.lck" inside holotree location, and renamed it to be `global.lck` file. - added environment variable `SSL_CERT_FILE` to point into certificate bundle if one is provided by profile - documentation updates --- common/variables.go | 2 +- common/version.go | 2 +- conda/robocorp.go | 1 + docs/README.md | 15 ++++++--- docs/changelog.md | 10 +++++- docs/recipes.md | 79 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 8 deletions(-) diff --git a/common/variables.go b/common/variables.go index 6087e0fe..45dd1784 100644 --- a/common/variables.go +++ b/common/variables.go @@ -173,7 +173,7 @@ func HololibUsageLocation() string { } func HolotreeLock() string { - return fmt.Sprintf("%s.lck", HolotreeLocation()) + return filepath.Join(HolotreeLocation(), "global.lck") } func UsesHolotree() bool { diff --git a/common/version.go b/common/version.go index 987d1b9c..6146ce3f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.2` + Version = `v11.26.3` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index f9348fe5..7936f587 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -145,6 +145,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if settings.Global.HasCaBundle() { environment = appendIfValue(environment, "REQUESTS_CA_BUNDLE", common.CaBundleFile()) environment = appendIfValue(environment, "CURL_CA_BUNDLE", common.CaBundleFile()) + environment = appendIfValue(environment, "SSL_CERT_FILE", common.CaBundleFile()) } return environment } diff --git a/docs/README.md b/docs/README.md index cf0152a9..d9622346 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,11 +56,16 @@ #### 3.15.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) #### 3.15.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) #### 3.15.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) -### 3.16 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.17 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.17.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.17.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.18 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +### 3.16 [How to setup custom templates?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-custom-templates) +#### 3.16.1 [Custom template configuration in `settings.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-in-settingsyaml-) +#### 3.16.2 [Custom template configuration file as `templates.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-file-as-templatesyaml-) +#### 3.16.3 [Custom template content in `templates.zip` file.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-content-in-templateszip-file) +#### 3.16.4 [Shared using `https:` protocol ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#shared-using-https-protocol-) +### 3.17 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.18 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.18.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.18.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.19 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) diff --git a/docs/changelog.md b/docs/changelog.md index c40294ac..db5c4b36 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.26.3 (date: 12.9.2022) + +- bugfix: moved "ht.lck" inside holotree location, and renamed it to be + `global.lck` file. +- added environment variable `SSL_CERT_FILE` to point into certificate bundle + if one is provided by profile +- documentation updates + ## v11.26.2 (date: 8.9.2022) - converted assets to embedded resources (golang builtin embed module) @@ -43,7 +51,7 @@ - added maintenance related robot test suite - minor documentation updates -## v11.22.1 (date: 1.9.2022) +## v11.22.1 (date: 1.9.2022) BROKEN - fix: using wrong file for age calculation on holotree catalogs - fix: holotree check failed to recover on corrupted files; now failure diff --git a/docs/recipes.md b/docs/recipes.md index 36d69de6..46f05a4c 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -774,6 +774,85 @@ compromising variable content inside repository). CI step recipe and not have external scripts (but you decide that) +## How to setup custom templates? + +Custom templates allows making your own templates that can be used when +new robot is created. So if you have your standard way of doing things, +then custom template is good way to codify it. + +You then need to do these steps: + +- setup custom settings.yaml that point location where template configuration + file is located (the templates.yaml file) +- create that custom templates.yaml configuration file that lists available + templates, and where template bundle can be found (the templates.zip file) +- and finally build that templates.zip to bundle together all those templates + that were listed in configuration file +- and finally both templates.yaml and templates.zip must be somewhere behind + URL that starts with https: + +Note: templates are needed only on development context, and they are not used +or needed in Assistant or Workforce Agent context. + +### Custom template configuration in `settings.yaml`. + +In settings.yaml, there is `autoupdates:` section, and there is entry for +`templates:` where you should put exact name and location where active +templates configuration file is located. + +Example: + +```yaml +autoupdates: + templates: https://special.acme.com/robot/templates-1.0.1.yaml +``` + +As above example shows, name is configurable, and can even contain some +versioning information, if so needed. + +### Custom template configuration file as `templates.yaml`. + +In that `templates.yaml` following things must be provided: + +- `hash:` (sha256) of "templates.zip" file (so that integrity of templates.zip + can be verified) +- `url:` to exact name and location where that templates.zip can be downloaded +- `date:` when this template.yaml file was last updated +- `templates:` as key/value pairs of templates and their "one liner" + description seen in UIs +- so, if there is `shell.zip` inside templates.zip, then that should have + `shell: Shell Robot Template` or something similar in that `templates:` + section + +Example: + +```yaml +hash: c7b1ba0863d9f7559de599e3811e31ddd7bdb72ce862d1a033f5396c92c5c4ec +url: https://special.acme.com/robot/templates-1.0.1.zip +date: 2022-09-12 +templates: + shell: Simple Shell Robot template + extended: Extended Robot Framework template + playwright: Playwright template + producer-consumer: Producer-consumer model template +``` + +### Custom template content in `templates.zip` file. + +Then that `templates.zip` is zip-of-zips. So for each key from templates.yaml +`templates:` sections should have matching .zip file inside that master zip. + +### Shared using `https:` protocol ... + +Then both `templates.yaml` and `templates.zip` should be hosted somewhere +which can be accessed using https protocol. Names there should match those +defined in above steps. + +And that `settings.yaml` should either be delivered standalone into those +developer machines that need to use those templates, or better yet, be part +of "profile" that developers can use to setup all of required configurations. + + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html From ada5d354c56705a54df6ae0cc15c37167c1fffbc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 14 Sep 2022 13:27:44 +0300 Subject: [PATCH 298/516] DOCS: history and troubleshooting (v11.26.4) --- common/version.go | 2 +- docs/README.md | 25 +++++- docs/changelog.md | 6 ++ docs/history.md | 188 ++++++++++++++++++++++++++++++++++++++++ docs/troubleshooting.md | 79 +++++++++++++++++ scripts/toc.py | 6 +- 6 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 docs/history.md create mode 100644 docs/troubleshooting.md diff --git a/common/version.go b/common/version.go index 6146ce3f..21ac30b8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.3` + Version = `v11.26.4` ) diff --git a/docs/README.md b/docs/README.md index d9622346..69a6cbf6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -80,7 +80,24 @@ #### 4.7.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) #### 4.7.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) #### 4.7.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## 5 [rcc -- how to build it](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#rcc----how-to-build-it) -### 5.1 [Tooling](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#tooling) -### 5.2 [Commands](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#commands) -### 5.3 [Where to start reading code?](https://github.com/robocorp/rcc/blob/master/docs/BUILD.md#where-to-start-reading-code) \ No newline at end of file +## 5 [Troubleshooting guidelines and known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#troubleshooting-guidelines-and-known-solutions) +### 5.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) +### 5.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) +### 5.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) +### 5.4 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) +#### 5.4.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) +#### 5.4.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) +## 6 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) +### 6.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) +### 6.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) +### 6.3 [Version 9.x: between Jan 15, 2021 and Jun 10, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-9x-between-jan-15-2021-and-jun-10-2021) +### 6.4 [Version 8.x: between Jan 4, 2021 and Jan 18, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-8x-between-jan-4-2021-and-jan-18-2021) +### 6.5 [Version 7.x: between Dec 1, 2020 and Jan 4, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-7x-between-dec-1-2020-and-jan-4-2021) +### 6.6 [Version 6.x: between Nov 16, 2020 and Nov 30, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-6x-between-nov-16-2020-and-nov-30-2020) +### 6.7 [Version 5.x: between Nov 4, 2020 and Nov 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-5x-between-nov-4-2020-and-nov-16-2020) +### 6.8 [Version 4.x: between Oct 20, 2020 and Nov 2, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-4x-between-oct-20-2020-and-nov-2-2020) +### 6.9 [Version 3.x: between Oct 15, 2020 and Oct 19, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-3x-between-oct-15-2020-and-oct-19-2020) +### 6.10 [Version 2.x: between Sep 16, 2020 and Oct 14, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-2x-between-sep-16-2020-and-oct-14-2020) +### 6.11 [Version 1.x: between Sep 3, 2020 and Sep 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-1x-between-sep-3-2020-and-sep-16-2020) +### 6.12 [Version 0.x: between April 1, 2020 and Sep 8, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-0x-between-april-1-2020-and-sep-8-2020) +### 6.13 [Birth of "Codename: Conman"](https://github.com/robocorp/rcc/blob/master/docs/history.md#birth-of-codename-conman) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index db5c4b36..f6bafd2b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.26.4 (date: 14.9.2022) + +- new `docs/troubleshooting.md` document added +- new `docs/history.md` document added +- updated `scripts/toc.py` with new documents and minor improvement + ## v11.26.3 (date: 12.9.2022) - bugfix: moved "ht.lck" inside holotree location, and renamed it to be diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 00000000..0de9259d --- /dev/null +++ b/docs/history.md @@ -0,0 +1,188 @@ +# History of rcc + +This is quick recap of rcc history. Just major topics and breaking changes. +There has already been 500+ commits, with lots of fixes and minor improvements, +and they are not listed here. + +## Version 11.x: between Sep 6, 2021 and ... + +Version "eleven" is work in progress and has already 100+ commits, and at least +following improvements: + +- Work In Progress ... + +## Version 10.x: between Jun 15, 2021 and Sep 1, 2021 + +Version "ten" had 32 commits, and had following improvements: + +- breaking change: removed lease support +- listing of dependencies is now part of holotree space (golden-ee.yaml) +- dependency listing is visible before run (to help debugging environment + changes) and there is also command to list them +- environment definitions can now be "freezed" using freeze file from run output +- supporting multiple environment configurations to enable operating system + and architecture specific freeze files (within one robot project) +- made environment creation serialization visible when multiple processes are + involved +- added holotree check command to verify holotree library integrity and remove + those items that are broken + +## Version 9.x: between Jan 15, 2021 and Jun 10, 2021 + +Version "nine" had 101 commits, and had following improvements: + +- breaking change: old "package.yaml" support was fully dropped +- breaking change: new lease option breaks contract of pristine environments in + cases where one application has already requested long living lease, and + other wants to use environment with exactly same specification +- new environment leasing options added +- added configuration diagnostics support to identify environment related issues +- diagnostics can also be done to robots, so that robot issues become visible +- experiment: carrier robots as standalone executables +- issue reporting support for applications (with dryrun options) +- removing environments now uses rename/delete pattern (for detecting locking + issues) +- environment based temporary folder management improvements +- added support for detecting when environment gets corrupted and showing + differences compared to pristine environment +- added support for execution timeline summary +- assistants environments can be prepared before they are used/needed, and this + means faster startup time for assistants +- environments are activated once, on creation (stored on `rcc_activate.json`) +- installation plan is also stored as `rcc_plan.log` inside environment and + there is command to show it +- introduction of `settings.yaml` file for configurable items +- introduced holotree command subtree into source code base +- holotree implementation is build parallel to existing environment management +- holotree now co-exists with old implementation in backward compatible way +- exporting holotrees as hololib.zip files is possible and robot can be executed + against it +- micromamba download is now done "on demand" only +- result of environment variables command are now directly executable +- execution can now be profiled "on demand" using command line flags +- download index is generated directly from changelog content +- started to use capability set with Cloud authorization +- new environment variable `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` to make + skip those system requirements that some users are willing to try +- new environment variable `RCC_VERBOSE_ENVIRONMENT_BUILDING` to make + environment building more verbose +- for `task run` and `task testrun` there is now possibility to give additional + arguments from commandline, by using `--` separator between normal rcc + arguments and those intended for executed robot +- added event journaling support, and command to see them +- added support to run scripts inside task environments + +## Version 8.x: between Jan 4, 2021 and Jan 18, 2021 + +Version "eight" had 14 commits, and had following improvements: + +- breaking change: 32-bit support was dropped +- automatic download and installation of micromamba +- fully migrated to micromamba and removed miniconda3 +- no more conda commands and also removed some conda variables +- now conda and pip installation steps are clearly separated + +## Version 7.x: between Dec 1, 2020 and Jan 4, 2021 + +Version "seven" had 17 commits, and had following improvements: + +- breaking change: switched to use sha256 as hashing algorithm +- changelogs are now held in separate file +- changelogs are embedded inside rcc binary +- started to introduce micromamba into project +- indentity.yaml is saved inside environment +- longpath checking and fixing for Windows introduced +- better cleanup support for items inside `ROBOCORP_HOME` + +## Version 6.x: between Nov 16, 2020 and Nov 30, 2020 + +Version "six" had 24 commits, and had following improvements: + +- breaking change: stdout is used for machine readable output, and all error + messages go to stderr including debug and trace outputs +- introduced postInstallScripts into conda.yaml +- interactive create for creating robots from templates + +## Version 5.x: between Nov 4, 2020 and Nov 16, 2020 + +Version "five" had 28 commits, and had following improvements: + +- breaking change: REST API server removed (since it is easier to use just as + CLI command from applications) +- Open Source repository for rcc created and work continued there (Nov 10) +- using Apache license as OSS license +- detecting interactive use and coloring outputs +- tutorial added as command +- added community pull and tooling support + +## Version 4.x: between Oct 20, 2020 and Nov 2, 2020 + +Version "four" had 12 commits, and had following improvements: + +- breaking change related to new assistant encryption scheme +- usability improvements on CLI use +- introduced "controller" concept as toplevel persistent option +- dynamic ephemeral account support introduced + +## Version 3.x: between Oct 15, 2020 and Oct 19, 2020 + +Version "three" had just 6 commits, and had following improvements: + +- breaking change was transition from "task" to "robotTaskName" in robot.yaml +- assistant heartbeat introduced +- lockless option introduced and better support for debugging locking support + +## Version 2.x: between Sep 16, 2020 and Oct 14, 2020 + +Version "two" had around 29 commits, and had following improvements: + +- URL (breaking) changes in Cloud required Major version upgrade +- added assistant support (list, run, download, upload artifacts) +- added support to execute "anything", no condaConfigFile required +- file locking introduced +- robot cache introduced at `$ROBOCORP_HOME/robots/` + +## Version 1.x: between Sep 3, 2020 and Sep 16, 2020 + +Version "one" had around 13 commits, and had following improvements: + +- terminology was changed, so code also needed to be changed +- package.yaml converted to robot.yaml +- packages were renamed to robots +- activities were renamed to tasks +- added support for environment cleanups +- added support for library management + +## Version 0.x: between April 1, 2020 and Sep 8, 2020 + +Even when project started as "conman", it was renamed to "rcc" on May 8, 2020. + +Initial "zero" version was around 120 commits and following highlevel things +were developed in that time: + +- cross-compiling to Mac, Linux, Windows, and Raspberry Pi +- originally supported were 32 and 64 bit architectures of arm and amd +- delivery as signed/notarized binaries in Mac and Windows +- download and install miniconda3 automatically +- management of separate environments +- using miniconda to manage packages at `ROBOCORP_HOME` +- merge support for multiple conda.yaml files +- initially using miniconda3 to create those environments +- where robots were initially defined in `package.yaml` +- packaging and unpacking of robots to and from zipped activity packages +- running robots (using run and testrun subcommands) +- local conda channels and pip wheels +- sending metrics to cloud +- CLI handling and command hierarchy using Viper and Cobra +- cloud communication using accounts, credentials, and tokens +- `ROBOCORP_HOME` variable as center of universe +- there was server support, and REST API for applications to use +- ignore files support +- support for embedded templates using go-bindata +- originally used locality-sensitive hashing for conda.yaml identity +- both Lab and Worker support + +## Birth of "Codename: Conman" + +First commit to private conman repo was done on April 1, 2020. And name was +shortening of "conda manager". And it was developer generated name. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..59fbab19 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,79 @@ +# Troubleshooting guidelines and known solutions + +> Help us to help you to resolve issue you are having. + +## Tools to help with troubleshooting issues + +- run command `rcc configuration diagnostics` and see if there are warnings + or errors in output +- if failure is with specific robot, then try running command + `rcc configuration diagnostics --robot path/to/robot.yaml` and see if + those robot diagnostics have something that identifies a problem +- run command `rcc configuration speedtest` to see, if problem is actually + performance related (like slow disk or network access) +- run rcc commands with `--debug` and `--timeline` flags, and see if anything + there adds more information on why failure is happening + +## How to troubleshoot issue you are having? + +- are you using latest versions of tools and libraries, and if not, then first + thing to do is update them and then retry +- if you have only encoutered the problem just once, try to repeat it, and + if you cannot repeat it, you are done +- has this ever before worked in this same user/machine/network combination, + and if not, then are you using correct profile and settings? +- if this worked previously, and then stopped working, what has changed or + what have you changed? any updates? new IT policies? new network location? +- gather evidence, that is all logs, console outputs, stack traces, screenshots, + and look them thru +- what is first error your see in console output, and what is last error your + see, then look between + +## Reporting an issue + +- describe what were you trying to achieve +- describe what did you actually do when trying to achieve it +- describe what did actually happen +- describe what were you expecting to happen +- describe what did happen that you think indicates that there is an issue +- describe what error messages did you see +- describe steps that are needed to be able to reproduce this issue +- describe what have you already tried to resolve this issue +- describe what has changed since this was not present and everything worked ok + +## Known solutions + +### Access denied while building holotree environment (Windows) + +If file is .dll or .exe file, then there is probably some process running, that +has actually locked that file, and tooling cannot complete its operation while +that other process is running. Other process might be virus scanner, some other +tool (Assistant, Workforce Agent, Automation Studio, VS Code, rcc) using same +environment, or even open Explorer view. + +To resolve this, close other applications, or wait them to finish before trying +same operation again. + +### Message "Serialized environment creation" repeats + +There can be few reasons for this. Here are some ways to resolve it. + +If multiple robots in same machine are trying to create new environment or +refresh existing one at exactly same time, then only one of them can continue. +This is there to protect integrity and security of holotree and hololib, and +also conserve resources for doing duplicate work. In this case, best thing to +resolve this is just to wait processes to complete. + +Other case is where there are multiple rcc processes running, but none of them +seems to be progressing. This might be indication that there is one "zombie" +process, which is holding on to a lock, and wont go away since some of its +child processes is still running (like python, web browser, or Excel). In this +case, best way is to close those "hanging" processes, and let OS to finish +that pending (and lock holding) process. + +Third case is where there seems to be only one rcc, and it is just waiting and +repeating that message. In this case it is probably a permission issue, and +for some reason .lck file is not accessible/lockable by rcc process. In this +case, you should go and look if current user has rights to actually modify +those .lck files, and if not, you have to grant them those. This might require +administrator privileges to actually change those file permissions. diff --git a/scripts/toc.py b/scripts/toc.py index 255c058f..1e515a07 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -5,7 +5,7 @@ from os.path import basename DELETE_PATTERN = re.compile(r'[/:]+') -NONCHAR_PATTERN = re.compile(r'[^.a-z-]+') +NONCHAR_PATTERN = re.compile(r'[^.a-z0-9-]+') HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') CODE_PATTERN = re.compile(r'^\s*[`]{3}') @@ -13,7 +13,7 @@ DASH = '-' NEWLINE = '\n' -IGNORE_LIST = ('changelog.md', 'toc.md', 'README.md') +IGNORE_LIST = ('changelog.md', 'toc.md', 'BUILD.md', 'README.md') PRIORITY_LIST = ( 'docs/usecases.md', @@ -21,6 +21,8 @@ 'docs/recipes.md', 'docs/profile_configuration.md', 'docs/environment-caching.md', + 'docs/troubleshooting.md', + 'docs/history.md', ) def unify(value): From 4d35c04c8b3690ac343766d2b770a045c78697da Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 16 Sep 2022 10:28:23 +0300 Subject: [PATCH 299/516] DOCS: metrics and documentation update (v11.26.5) - added architecture/platform metric with same interval as timezone metrics - `docs/history.md` updated with v11 information so far - `docs/troubleshooting.md` updated with additional points --- cmd/rcc/main.go | 2 ++ common/version.go | 2 +- docs/changelog.md | 6 ++++++ docs/history.md | 36 +++++++++++++++++++++++++++++++++++- docs/troubleshooting.md | 4 ++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 25d90341..6a9f3509 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -16,6 +16,7 @@ import ( const ( timezonekey = `rcc.cli.tz` + oskey = `rcc.cli.os` daily = 60 * 60 * 24 ) @@ -35,6 +36,7 @@ func TimezoneMetric() error { cache.Stamps[timezonekey] = common.When + daily zone := time.Now().Format("MST-0700") cloud.BackgroundMetric(common.ControllerIdentity(), timezonekey, zone) + cloud.BackgroundMetric(common.ControllerIdentity(), oskey, common.Platform()) return cache.Save() } diff --git a/common/version.go b/common/version.go index 21ac30b8..29fa2c8f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.4` + Version = `v11.26.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index f6bafd2b..e048c90d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.26.5 (date: 16.9.2022) + +- added architecture/platform metric with same interval as timezone metrics +- `docs/history.md` updated with v11 information so far +- `docs/troubleshooting.md` updated with additional points + ## v11.26.4 (date: 14.9.2022) - new `docs/troubleshooting.md` document added diff --git a/docs/history.md b/docs/history.md index 0de9259d..6767441a 100644 --- a/docs/history.md +++ b/docs/history.md @@ -9,7 +9,41 @@ and they are not listed here. Version "eleven" is work in progress and has already 100+ commits, and at least following improvements: -- Work In Progress ... +- breaking change: old environment caching (base/live) was fully removed and + holotree is only solution available +- breaking change: hashing algorithm changed, holotree uses siphash fron now on +- environment section of commands were removed, replacements live in holotree + section +- environment cleanup changed, since holotree is different from base/live envs +- auto-scaling worker count is now based on number of CPUs minus one, but at + least two and maximum of 96 +- templates can now be automatically updated from Cloud and can also be + customized using settings.yaml autoupdates section +- added option to do strict environment building, which turns pip warnings + into actual errors +- added support for speed test, where current machine performance gets scored +- hololib.zip files can now be imported into normal holotree library (allows + air gapped workflow) +- added more commands around holotree implementation +- added support for preRunScripts, which are executed in similar context that + actual robot will use, and there can be OS specific scripts only run on + that specific OS +- added profile support with define, export, import, and switch functionality +- certificate bundle, micromambarc, piprc, and settings can be part of profile +- `settings.yaml` now has layers, so that partial settings are possible, and + undefined ones use internal default settings +- `docs/` folder has generated "table of content" +- introduced "shared holotree", where multiple users in same computer can + share resources needed by holotree spaces +- in addition to normal tasks, now robot.yaml can also contain devTasks, which + can be activated with flag `--dev` +- holotrees can also be imported directly from URLs +- some experimental support for virtual environments (pyvenv.cfg and others) +- moved from "go-bindata" to use new go buildin "embed" module +- holotree now also fully support symbolic links inside created environments +- improved cleanup in relation to new shared holotrees +- individual catalog removal and cleanup is now possible +- prebuild environments can now be forced using "no build" configurations ## Version 10.x: between Jun 15, 2021 and Sep 1, 2021 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 59fbab19..1d5d3c2e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -40,6 +40,10 @@ - describe steps that are needed to be able to reproduce this issue - describe what have you already tried to resolve this issue - describe what has changed since this was not present and everything worked ok +- you should share your `conda.yaml` used with robot or environment +- you should share your `robot.yaml` that defines your robot +- you should share your code, or minimal sample code, that can reproduce + problem you are having ## Known solutions From 3dfa04701171a971c6a9c241731d910591fe2101 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 19 Sep 2022 15:35:32 +0300 Subject: [PATCH 300/516] SECURITY: dependency updates (v11.26.6) - try to upgraded cobra and viper dependencies, to get remove security warnings given by AWS container scanner tooling - upgrade to use github.com/spf13/cobra v1.5.0 - upgrade to use github.com/spf13/viper v1.13.0 - upgrade to use gopkg.in/square/go-jose.v2 v2.6.0 --- common/version.go | 2 +- docs/changelog.md | 8 + go.mod | 34 +-- go.sum | 518 ++++++++++++++++++++++++++++++---------------- 4 files changed, 363 insertions(+), 199 deletions(-) diff --git a/common/version.go b/common/version.go index 29fa2c8f..66175b29 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.5` + Version = `v11.26.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index e048c90d..c891cfb7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.26.6 (date: 19.9.2022) + +- try to upgraded cobra and viper dependencies, to get remove security warnings + given by AWS container scanner tooling +- upgrade to use github.com/spf13/cobra v1.5.0 +- upgrade to use github.com/spf13/viper v1.13.0 +- upgrade to use gopkg.in/square/go-jose.v2 v2.6.0 + ## v11.26.5 (date: 16.9.2022) - added architecture/platform metric with same interval as timezone metrics diff --git a/go.mod b/go.mod index fce15ee4..0af70013 100644 --- a/go.mod +++ b/go.mod @@ -5,28 +5,30 @@ go 1.18 require ( github.com/dchest/siphash v1.2.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/mattn/go-isatty v0.0.12 - github.com/spf13/cobra v0.0.7 - github.com/spf13/viper v1.7.1 - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 + github.com/mattn/go-isatty v0.0.14 + github.com/spf13/cobra v1.5.0 + github.com/spf13/viper v1.13.0 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b - gopkg.in/square/go-jose.v2 v2.5.1 - gopkg.in/yaml.v2 v2.2.8 + gopkg.in/square/go-jose.v2 v2.6.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/magiconair/properties v1.8.1 // indirect - github.com/mitchellh/mapstructure v1.2.2 // indirect - github.com/pelletier/go-toml v1.6.0 // indirect - github.com/spf13/afero v1.2.2 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect - golang.org/x/text v0.3.2 // indirect - gopkg.in/ini.v1 v1.55.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3e27b8ca..9f25e978 100644 --- a/go.sum +++ b/go.sum @@ -3,222 +3,211 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= -github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -228,39 +217,71 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -268,45 +289,127 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -316,29 +419,80 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= -gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 98fcc7ac0314c3831063e93be74d0c582008b6f1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 23 Sep 2022 16:49:45 +0300 Subject: [PATCH 301/516] FEATURE: installation plan analysis (v11.27.0) - support for analyzing installation plans and their challenges and show it online, or afterwards - analysis is visible in `rcc holotree plan` command and also in `pip` phase in environment creation --- cmd/holotreePlan.go | 12 ++-- common/version.go | 2 +- conda/activate.go | 4 +- conda/plananalyzer.go | 147 ++++++++++++++++++++++++++++++++++++++++++ conda/workflows.go | 19 ++++-- docs/changelog.md | 7 ++ 6 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 conda/plananalyzer.go diff --git a/cmd/holotreePlan.go b/cmd/holotreePlan.go index e3b04d65..e0324de9 100644 --- a/cmd/holotreePlan.go +++ b/cmd/holotreePlan.go @@ -1,10 +1,10 @@ package cmd import ( - "fmt" - "io/ioutil" + "io" "os" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" @@ -23,9 +23,13 @@ var holotreePlanCmd = &cobra.Command{ for _, label := range htfs.FindEnvironment(prefix) { planfile, ok := htfs.InstallationPlan(label) pretty.Guard(ok, 1, "Could not find plan for: %v", label) - content, err := ioutil.ReadFile(planfile) + source, err := os.Open(planfile) pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) - fmt.Fprintf(os.Stdout, string(content)) + defer source.Close() + analyzer := conda.NewPlanAnalyzer(false) + defer analyzer.Close() + sink := io.MultiWriter(os.Stdout, analyzer) + io.Copy(sink, source) found = true } } diff --git a/common/version.go b/common/version.go index 66175b29..102a6793 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.26.6` + Version = `v11.27.0` ) diff --git a/conda/activate.go b/conda/activate.go index 7e7466a2..7f7779a2 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -5,8 +5,8 @@ import ( "encoding/json" "fmt" "html/template" + "io" "io/ioutil" - "os" "path/filepath" "strings" @@ -98,7 +98,7 @@ func diffStringMaps(before, after map[string]string) map[string]string { return result } -func Activate(sink *os.File, targetFolder string) error { +func Activate(sink io.Writer, targetFolder string) error { envCommand := []string{common.BinRcc(), "internal", "env", "--label", "before"} out, _, err := LiveCapture(targetFolder, envCommand...) if err != nil { diff --git a/conda/plananalyzer.go b/conda/plananalyzer.go new file mode 100644 index 00000000..8b201f68 --- /dev/null +++ b/conda/plananalyzer.go @@ -0,0 +1,147 @@ +package conda + +import ( + "bytes" + "fmt" + "regexp" + "strings" + "time" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" +) + +const ( + newline = '\n' + spacing = "\r\n\t " +) + +var ( + planPattern = regexp.MustCompile("^--- (.+?) plan @\\d+.\\d+s ---$") +) + +type ( + AnalyzerStrategy func(*PlanAnalyzer, string) + StrategyMap map[string]AnalyzerStrategy + RepeatCache map[string]bool + + PlanAnalyzer struct { + Strategies StrategyMap + Active AnalyzerStrategy + Notes []string + Pending []byte + Repeats RepeatCache + Realtime bool + Details bool + Started time.Time + } +) + +func NewPlanAnalyzer(realtime bool) *PlanAnalyzer { + strategies := make(StrategyMap) + strategies["micromamba"] = ignoreStrategy + strategies["post install"] = ignoreStrategy + strategies["activation"] = ignoreStrategy + strategies["pip check"] = ignoreStrategy + strategies["pip"] = pipStrategy + return &PlanAnalyzer{ + Strategies: strategies, + Active: ignoreStrategy, + Notes: []string{}, + Pending: nil, + Repeats: make(RepeatCache), + Realtime: realtime, + Details: false, + } +} + +func pipStrategy(ref *PlanAnalyzer, event string) { + low := strings.ToLower(event) + note := "" + detail := "" + if strings.HasPrefix(low, "info:") || strings.HasPrefix(low, "error:") { + note = event + } + if strings.Contains(low, "using cached") { + if strings.Contains(low, ".tar.gz") { + detail = fmt.Sprintf("%s [missing wheel file?]", event) + } else { + detail = event + } + } + elapsed := time.Since(ref.Started).Round(1 * time.Second) + if len(note) > 0 { + ref.Notes = append(ref.Notes, note) + if ref.Realtime { + pretty.Warning("%s @%s", note, elapsed) + } + ref.Details = true + return + } + if ref.Details && len(detail) > 0 { + ref.Notes = append(ref.Notes, detail) + if ref.Realtime { + pretty.Note("%s @%s", detail, elapsed) + } + return + } + if ref.Realtime { + common.Trace("PIP: %s", event) + } +} + +func ignoreStrategy(ref *PlanAnalyzer, event string) { + // does nothing by default +} + +func (it *PlanAnalyzer) Observe(event string) { + found := planPattern.FindStringSubmatch(event) + if len(found) > 1 { + it.Active = ignoreStrategy + strategy, ok := it.Strategies[found[1]] + if ok { + it.Active = strategy + } + it.Repeats = make(RepeatCache) + it.Details = false + it.Started = time.Now() + } + it.Active(it, event) +} + +func (it *PlanAnalyzer) Write(blob []byte) (int, error) { + old := len(it.Pending) + update := len(blob) + body := make([]byte, 0, old+update) + if old > 0 { + body = append(body, it.Pending...) + } + if update > 0 { + body = append(body, blob...) + } + terminator := []byte{newline} + parts := bytes.SplitAfter(body, terminator) + size := len(parts) + last := parts[size-1] + terminated := bytes.HasSuffix(last, terminator) + if !terminated { + it.Pending = last + parts = parts[:size-1] + } else { + it.Pending = nil + } + for _, part := range parts { + it.Observe(strings.TrimRight(string(part), spacing)) + } + return update, nil +} + +func (it *PlanAnalyzer) Close() { + if len(it.Notes) == 0 || it.Realtime { + return + } + pretty.Warning("Analyzing installation plan revealed following findings:") + for _, note := range it.Notes { + common.Log(" %s* %s%s%s", pretty.Cyan, pretty.Bold, note, pretty.Reset) + } +} diff --git a/conda/workflows.go b/conda/workflows.go index 2e7c7c95..50ca4170 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -51,8 +51,7 @@ func LiveCapture(liveFolder string, command ...string) (string, int, error) { return task.CaptureOutput() } -func LiveExecution(sink *os.File, liveFolder string, command ...string) (int, error) { - defer sink.Sync() +func LiveExecution(sink io.Writer, liveFolder string, command ...string) (int, error) { fmt.Fprintf(sink, "Command %q at %q:\n", command, liveFolder) task, err := livePrepare(liveFolder, command...) if err != nil { @@ -121,18 +120,23 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := common.StageFolder planfile := fmt.Sprintf("%s.plan", targetFolder) - planWriter, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + planSink, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return false, false } defer func() { - planWriter.Close() + planSink.Close() content, err := ioutil.ReadFile(planfile) if err == nil { common.Log("%s", string(content)) } os.Remove(planfile) }() + + planalyzer := NewPlanAnalyzer(true) + defer planalyzer.Close() + + planWriter := io.MultiWriter(planSink, planalyzer) fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) stopwatch := common.Stopwatch("installation plan") fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) @@ -178,6 +182,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip install phase ===") code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") @@ -200,6 +205,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } common.Debug("Running post install script '%s' ...", script) _, err = LiveExecution(planWriter, targetFolder, scriptCommand...) + planSink.Sync() if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) @@ -230,6 +236,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip check fail.") @@ -241,8 +248,8 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Progress(9, "Pip check skipped.") } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) - planWriter.Sync() - planWriter.Close() + planSink.Sync() + planSink.Close() common.Progress(10, "Update installation plan.") finalplan := filepath.Join(targetFolder, "rcc_plan.log") os.Rename(planfile, finalplan) diff --git a/docs/changelog.md b/docs/changelog.md index c891cfb7..7eb9deb4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.27.0 (date: 23.9.2022) + +- support for analyzing installation plans and their challenges and show it + online, or afterwards +- analysis is visible in `rcc holotree plan` command and also in `pip` + phase in environment creation + ## v11.26.6 (date: 19.9.2022) - try to upgraded cobra and viper dependencies, to get remove security warnings From 21f52521c83d806afa53dda15da84db4f4abc70c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 26 Sep 2022 09:57:56 +0300 Subject: [PATCH 302/516] FIX: plan analysis security fix (v11.27.1) - fixing CodeQL security warning about allocation overflow --- common/version.go | 2 +- conda/plananalyzer.go | 3 ++- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 102a6793..2ee5bb3c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.27.0` + Version = `v11.27.1` ) diff --git a/conda/plananalyzer.go b/conda/plananalyzer.go index 8b201f68..bd71515a 100644 --- a/conda/plananalyzer.go +++ b/conda/plananalyzer.go @@ -112,7 +112,8 @@ func (it *PlanAnalyzer) Observe(event string) { func (it *PlanAnalyzer) Write(blob []byte) (int, error) { old := len(it.Pending) update := len(blob) - body := make([]byte, 0, old+update) + var total uint64 = uint64(old) + uint64(update) + body := make([]byte, 0, total) if old > 0 { body = append(body, it.Pending...) } diff --git a/docs/changelog.md b/docs/changelog.md index 7eb9deb4..1e94adeb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.27.1 (date: 26.9.2022) + +- fixing CodeQL security warning about allocation overflow + ## v11.27.0 (date: 23.9.2022) - support for analyzing installation plans and their challenges and show it From fb43a7ceaaf5de05907c19408cd5c5ebcfc2dc3b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 27 Sep 2022 12:26:19 +0300 Subject: [PATCH 303/516] UPDATE: plan analysis improvements (v11.27.2) - improving plan analyzer with more rules to show messages --- common/version.go | 2 +- conda/plananalyzer.go | 35 +++++++++++++++++++++++++++++------ docs/changelog.md | 4 ++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index 2ee5bb3c..05405448 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.27.1` + Version = `v11.27.2` ) diff --git a/conda/plananalyzer.go b/conda/plananalyzer.go index bd71515a..2bd7d6c0 100644 --- a/conda/plananalyzer.go +++ b/conda/plananalyzer.go @@ -17,7 +17,18 @@ const ( ) var ( - planPattern = regexp.MustCompile("^--- (.+?) plan @\\d+.\\d+s ---$") + planPattern = regexp.MustCompile("^--- (.+?) plan @\\d+.\\d+s ---$") + pipNotePrefixes = [][2]string{ + {"info:", "%s [plan analyzer]"}, + {"warning:", "%s [plan analyzer]"}, + {"error:", "%s [plan analyzer]"}, + {"successfully uninstalled", "%s [plan analyzer: pip overrides conda]"}, + {"building wheels", "%s [plan analyzer: missing pip wheel files]"}, + } + pipNoteContains = [][2]string{} + pipDetailContains = [][2]string{ + {"which is incompatible", "%s [plan analyzer: pip vs. conda?]"}, + } ) type ( @@ -56,11 +67,18 @@ func NewPlanAnalyzer(realtime bool) *PlanAnalyzer { } func pipStrategy(ref *PlanAnalyzer, event string) { - low := strings.ToLower(event) + low := strings.TrimSpace(strings.ToLower(event)) note := "" detail := "" - if strings.HasPrefix(low, "info:") || strings.HasPrefix(low, "error:") { - note = event + for _, marker := range pipNotePrefixes { + if strings.HasPrefix(low, marker[0]) { + note = fmt.Sprintf(marker[1], event) + } + } + for _, marker := range pipNoteContains { + if strings.Contains(low, marker[0]) { + note = fmt.Sprintf(marker[1], event) + } } if strings.Contains(low, "using cached") { if strings.Contains(low, ".tar.gz") { @@ -69,11 +87,16 @@ func pipStrategy(ref *PlanAnalyzer, event string) { detail = event } } + for _, marker := range pipDetailContains { + if strings.Contains(low, marker[0]) { + detail = fmt.Sprintf(marker[1], event) + } + } elapsed := time.Since(ref.Started).Round(1 * time.Second) if len(note) > 0 { ref.Notes = append(ref.Notes, note) if ref.Realtime { - pretty.Warning("%s @%s", note, elapsed) + pretty.Warning("%s @%s", strings.TrimSpace(note), elapsed) } ref.Details = true return @@ -143,6 +166,6 @@ func (it *PlanAnalyzer) Close() { } pretty.Warning("Analyzing installation plan revealed following findings:") for _, note := range it.Notes { - common.Log(" %s* %s%s%s", pretty.Cyan, pretty.Bold, note, pretty.Reset) + common.Log(" %s* %s%s%s", pretty.Cyan, pretty.Bold, strings.TrimSpace(note), pretty.Reset) } } diff --git a/docs/changelog.md b/docs/changelog.md index 1e94adeb..c39f2667 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.27.2 (date: 27.9.2022) + +- improving plan analyzer with more rules to show messages + ## v11.27.1 (date: 26.9.2022) - fixing CodeQL security warning about allocation overflow From 0c797c7569b1ce38d30ccd8e7320d98767044db9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 29 Sep 2022 14:51:49 +0300 Subject: [PATCH 304/516] FIX: cloud new json output (v11.27.3) - fix: adding more "plan analyzer" identifiers to its output - fix: adding detection to "failed to build" messages - fix: added json output to new robot creation to cloud --- cmd/cloudNew.go | 13 ++++++++++++- common/version.go | 2 +- conda/plananalyzer.go | 8 +++++--- docs/changelog.md | 6 ++++++ robot_tests/holotree.robot | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index dd806a09..68d66602 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -1,6 +1,10 @@ package cmd import ( + "encoding/json" + "fmt" + "os" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" @@ -29,7 +33,13 @@ var newCloudCmd = &cobra.Command{ if err != nil { pretty.Exit(3, "Error: %v", err) } - common.Log("Created new robot named '%s' with identity %s.", reply["name"], reply["id"]) + if jsonFlag { + result, err := json.MarshalIndent(reply, "", " ") + pretty.Guard(err == nil, 1, "Json converion failed, reason: %v", err) + fmt.Fprintf(os.Stdout, "%s\n", result) + } else { + common.Log("Created new robot named '%s' with identity %s.", reply["name"], reply["id"]) + } }, } @@ -39,4 +49,5 @@ func init() { newCloudCmd.MarkFlagRequired("robot") newCloudCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use as creation target.") newCloudCmd.MarkFlagRequired("workspace") + newCloudCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") } diff --git a/common/version.go b/common/version.go index 05405448..e837dc79 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.27.2` + Version = `v11.27.3` ) diff --git a/conda/plananalyzer.go b/conda/plananalyzer.go index 2bd7d6c0..abcba10b 100644 --- a/conda/plananalyzer.go +++ b/conda/plananalyzer.go @@ -25,7 +25,9 @@ var ( {"successfully uninstalled", "%s [plan analyzer: pip overrides conda]"}, {"building wheels", "%s [plan analyzer: missing pip wheel files]"}, } - pipNoteContains = [][2]string{} + pipNoteContains = [][2]string{ + {"failed to build", "%s [plan analyzer: build failure]"}, + } pipDetailContains = [][2]string{ {"which is incompatible", "%s [plan analyzer: pip vs. conda?]"}, } @@ -82,9 +84,9 @@ func pipStrategy(ref *PlanAnalyzer, event string) { } if strings.Contains(low, "using cached") { if strings.Contains(low, ".tar.gz") { - detail = fmt.Sprintf("%s [missing wheel file?]", event) + detail = fmt.Sprintf("%s [plan analyzer: missing wheel file?]", event) } else { - detail = event + detail = fmt.Sprintf("%s [plan analyzer]", event) } } for _, marker := range pipDetailContains { diff --git a/docs/changelog.md b/docs/changelog.md index c39f2667..a14fa7eb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.27.3 (date: 29.9.2022) + +- fix: adding more "plan analyzer" identifiers to its output +- fix: adding detection to "failed to build" messages +- fix: added json output to new robot creation to cloud + ## v11.27.2 (date: 27.9.2022) - improving plan analyzer with more rules to show messages diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index b81d413e..67db9b73 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -37,7 +37,7 @@ Goal: See variables from specific environment without robot.yaml knowledge Wont Have ROBOT_ARTIFACTS= Goal: See variables from specific environment with robot.yaml but without task - Step build/rcc holotree variables --space jam --controller citests -r tmp/holotin/robot.yaml + Step build/rcc holotree variables --space holotin --controller citests -r tmp/holotin/robot.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc From 0b37323ed5e72e87c677ee93400bdf9d15fcc50d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 5 Oct 2022 12:51:30 +0300 Subject: [PATCH 305/516] UPGRADE: to micromamba v0.27.0 (v11.28.0) --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 33 +++++++++++++++++++++++++++++++-- conda/robocorp_test.go | 7 +++++++ conda/workflows.go | 18 ++++++++++++++---- docs/changelog.md | 8 ++++++++ 8 files changed, 64 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index e837dc79..472ed316 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.27.3` + Version = `v11.28.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index b77b9504..d1fbc872 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.25.1/macos64/micromamba") + return settings.Global.DownloadsLink(micromambaLink("macos64", "micromamba")) } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 21f4c03a..55d35baa 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.25.1/linux64/micromamba") + return settings.Global.DownloadsLink(micromambaLink("linux64", "micromamba")) } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index c8292bff..c1fab164 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.25.1/windows64/micromamba.exe") + return settings.Global.DownloadsLink(micromambaLink("windows64", "micromamba.exe")) } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 7936f587..cd9a4f46 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -17,6 +17,12 @@ import ( "github.com/robocorp/rcc/xviper" ) +const ( + // for micromamba upgrade, change following constants to match + micromambaVersionLimit = 27000 + micromambaVersionNumber = "v0.27.0" +) + var ( ignoredPaths = []string{ "python", @@ -27,9 +33,13 @@ var ( "virtualenv", } hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") - versionPattern = regexp.MustCompile("^[^0-9.]*([0-9.]+)\\s*$") + versionPattern = regexp.MustCompile("^[^0-9]*([0-9.]+).*$") ) +func micromambaLink(platform, filename string) string { + return fmt.Sprintf("micromamba/%s/%s/%s", micromambaVersionNumber, platform, filename) +} + func sorted(files []os.FileInfo) { sort.SliceStable(files, func(left, right int) bool { return files[left].Name() < files[right].Name() @@ -88,6 +98,15 @@ func FindPath(environment string) pathlib.PathParts { return target } +func FindPython(location string) (string, bool) { + holotreePath := HolotreePath(location) + python, ok := holotreePath.Which("python3", FileExtensions) + if ok { + return python, ok + } + return holotreePath.Which("python", FileExtensions) +} + func CondaExecutionEnvironment(location string, inject []string, full bool) []string { environment := make([]string, 0, 100) if full { @@ -185,6 +204,16 @@ search: return version, versionText } +func PipVersion(python string) string { + environment := CondaExecutionEnvironment(".", nil, true) + versionText, _, err := shell.New(environment, ".", python, "-m", "pip", "--version").CaptureOutput() + if err != nil { + return err.Error() + } + _, versionText = AsVersion(versionText) + return versionText +} + func MicromambaVersion() string { versionText, _, err := shell.New(CondaEnvironment(), ".", BinMicromamba(), "--repodata-ttl", "90000", "--version").CaptureOutput() if err != nil { @@ -199,7 +228,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= 25001 + goodEnough := version >= micromambaVersionLimit common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/conda/robocorp_test.go b/conda/robocorp_test.go index e830fe22..d31d2e29 100644 --- a/conda/robocorp_test.go +++ b/conda/robocorp_test.go @@ -20,3 +20,10 @@ func TestCanParseMicromambaVersion(t *testing.T) { must_be.Equal("0.19.0", second(conda.AsVersion("\n\n\tmicromamba: 0.19.0 \nlibmamba: 0.18.7\n\n\t"))) must_be.Equal("0.20", second(conda.AsVersion("microrumba: 0.20"))) } + +func TestCanParsePipVersion(t *testing.T) { + must_be, _ := hamlet.Specifications(t) + + must_be.Equal("20.3.4", second(conda.AsVersion("pip 20.3.4 from /outer/space/python/blah (python 3.9)"))) + must_be.Equal("22.2.2", second(conda.AsVersion("pip 22.2.2 from /outer/space/python/blah (python 3.9)"))) +} diff --git a/conda/workflows.go b/conda/workflows.go index 50ca4170..8647e4b9 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -147,7 +147,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - common.Progress(5, "Running micromamba phase.") + common.Progress(5, "Running micromamba phase. (micromamba v%s)", MicromambaVersion()) mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -169,14 +169,24 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, true } fmt.Fprintf(planWriter, "\n--- pip plan @%ss ---\n\n", stopwatch) + python, pyok := FindPython(targetFolder) + if !pyok { + fmt.Fprintf(planWriter, "Note: no python in target folder: %s\n", targetFolder) + } pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { common.Progress(6, "Skipping pip install phase -- no pip dependencies.") } else { - common.Progress(6, "Running pip install phase.") + if !pyok { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) + common.Timeline("pip fail. no python found.") + common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) + return false, false + } + common.Progress(6, "Running pip install phase. (pip v%s)", PipVersion(python)) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) - pipCommand := common.NewCommander("pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) + pipCommand := common.NewCommander(python, "-m", "pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -232,7 +242,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "\n--- pip check plan @%ss ---\n\n", stopwatch) if common.StrictFlag && pipUsed { common.Progress(9, "Running pip check phase.") - pipCommand := common.NewCommander("pip", "check", "--no-color") + pipCommand := common.NewCommander(python, "-m", "pip", "check", "--no-color") pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) diff --git a/docs/changelog.md b/docs/changelog.md index a14fa7eb..db3e225f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.28.0 (date: 5.10.2022) + +- micromamba upgrade to v0.27.0 +- refactored version micromamba version numbering into one place +- added used pip and micromamba versions in progress messages +- BUGFIX: now explicitely using environment python to run pip commands + (using `python -m pip install ...` form instead old `pip install` form) + ## v11.27.3 (date: 29.9.2022) - fix: adding more "plan analyzer" identifiers to its output From 3b38937e0b200926f1ea5352e5aa7302bc29c4ca Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 12 Oct 2022 08:44:04 +0300 Subject: [PATCH 306/516] BUGFIX: template update bugfix (v11.28.1) - bugfix: direct initializing robot did not update templates --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/initialize.go | 14 ++++++++++---- robot_tests/bug_reports.robot | 2 -- robot_tests/export_holozip.robot | 14 +++++++++----- robot_tests/supporting.py | 6 ++++++ 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/common/version.go b/common/version.go index 472ed316..4dd09e4f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.28.0` + Version = `v11.28.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index db3e225f..1e4164f3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.28.1 (date: 12.10.2022) + +- bugfix: direct initializing robot did not update templates + ## v11.28.0 (date: 5.10.2022) - micromamba upgrade to v0.27.0 diff --git a/operations/initialize.go b/operations/initialize.go index 027e8d8b..a25ab2f8 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -124,6 +124,13 @@ func downloadTemplatesZip(meta *MetaTemplates) (err error) { return nil } +func ensureUpdatedTemplates() { + err := updateTemplates() + if err != nil { + pretty.Warning("Problem updating templates.zip, reason: %v", err) + } +} + func updateTemplates() (err error) { defer fail.Around(&err) @@ -172,10 +179,7 @@ func unpack(content []byte, directory string) error { } func ListTemplatesWithDescription(internal bool) StringPairList { - err := updateTemplates() - if err != nil { - pretty.Warning("Problem updating templates.zip, reason: %v", err) - } + ensureUpdatedTemplates() result := make(StringPairList, 0, 10) meta, err := activeTemplateInfo(internal) if err != nil { @@ -190,6 +194,7 @@ func ListTemplatesWithDescription(internal bool) StringPairList { } func ListTemplates(internal bool) []string { + ensureUpdatedTemplates() pairs := ListTemplatesWithDescription(internal) result := make([]string, 0, len(pairs)) for _, pair := range pairs { @@ -221,6 +226,7 @@ func templateByName(name string, internal bool) ([]byte, error) { } func InitializeWorkarea(directory, name string, internal, force bool) error { + ensureUpdatedTemplates() content, err := templateByName(name, internal) if err != nil { return err diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index 795a20c1..65bfaaae 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -32,8 +32,6 @@ Bug in virtual holotree with gzipped files Must Have Blueprint "8b2083d262262cbd" is available: true Github issue 32 about rcc task script command failing - [Tags] WIP - Step build/rcc task script --controller citests --robot robot_tests/spellbug/robot.yaml -- pip list Use STDOUT Must Have pyspellchecker diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 4a97a057..3dd321f7 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -4,6 +4,7 @@ Library supporting.py Resource resources.robot Suite Setup Export setup Suite Teardown Export teardown +Default tags WIP *** Keywords *** Export setup @@ -26,6 +27,9 @@ Goal: Create extended robot into tmp/standalone folder using force. Use STDERR Must Have OK. + ${output}= Capture Flat Output build/rcc ht hash --silent tmp/standalone/conda.yaml + Set Suite Variable ${fingerprint} ${output} + Goal: Create environment for standalone robot Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml Must Have RCC_ENVIRONMENT_HASH= @@ -41,18 +45,18 @@ Goal: Must have author space visible Must Have 4e67cd8_fcb4b859 Must Have rcc.citests Must Have author - Must Have 1cdd0b852854fe5b + Must Have ${fingerprint} Wont Have guest Goal: Show exportable environment list Step build/rcc ht export Use STDERR Must Have Selectable catalogs - Must Have - 1cdd0b852854fe5b + Must Have - ${fingerprint} Must Have OK. Goal: Export environment for standalone robot - Step build/rcc ht export -z tmp/standalone/hololib.zip 1cdd0b852854fe5b + Step build/rcc ht export -z tmp/standalone/hololib.zip ${fingerprint} Use STDERR Wont Have Selectable catalogs Must Have OK. @@ -75,7 +79,7 @@ Goal: Can delete author space Wont Have 4e67cd8_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have 1cdd0b852854fe5b + Wont Have ${fingerprint} Wont Have guest Goal: Can run as guest @@ -92,6 +96,6 @@ Goal: Space created under author for guest Wont Have 4e67cd8_fcb4b859 Wont Have author Must Have rcc.citests - Must Have 1cdd0b852854fe5b + Must Have ${fingerprint} Must Have 4e67cd8_aacf1552 Must Have guest diff --git a/robot_tests/supporting.py b/robot_tests/supporting.py index f2ea9405..bbfdbded 100644 --- a/robot_tests/supporting.py +++ b/robot_tests/supporting.py @@ -1,6 +1,12 @@ import json import subprocess +def capture_flat_output(command): + task = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + out, _ = task.communicate() + assert task.returncode == 0, f'Unexpected exit code {task.returncode} from {command!r}' + return out.decode().strip() + def run_and_return_code_output_error(command): task = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, err = task.communicate() From b059a888d1ecf3c4afc475e4b5eba0aacaf96199 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 Oct 2022 13:24:00 +0300 Subject: [PATCH 307/516] IMPROVEMENT: lock wait messages (v11.28.2) - made lock wait messages little more descriptive and added more of them - added "pids" folder to keep track who is holding locks (just information) --- common/variables.go | 4 ++++ common/version.go | 2 +- conda/cleanup.go | 2 ++ conda/workflows.go | 4 ++-- docs/changelog.md | 5 +++++ htfs/commands.go | 4 ++-- htfs/directory.go | 1 + htfs/library.go | 2 ++ htfs/virtual.go | 2 ++ htfs/ziplibrary.go | 2 ++ operations/cache.go | 4 ++++ pathlib/lock.go | 18 +++++++++++++++++- pathlib/lock_unix.go | 6 +++++- pathlib/lock_windows.go | 5 ++++- xviper/wrapper.go | 4 ++++ 15 files changed, 57 insertions(+), 8 deletions(-) diff --git a/common/variables.go b/common/variables.go index 45dd1784..1b204421 100644 --- a/common/variables.go +++ b/common/variables.go @@ -160,6 +160,10 @@ func HololibLocation() string { return filepath.Join(RobocorpHome(), "hololib") } +func HololibPids() string { + return filepath.Join(HololibLocation(), "pids") +} + func HololibCatalogLocation() string { return filepath.Join(HololibLocation(), "catalog") } diff --git a/common/version.go b/common/version.go index 4dd09e4f..1460a25e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.28.1` + Version = `v11.28.2` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 638953dd..9ad45a45 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -135,7 +135,9 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error { lockfile := common.RobocorpLock() + completed := pathlib.LockWaitMessage("Serialized environment cleanup [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000) + completed() if err != nil { common.Log("Could not get lock on live environment. Quitting!") return err diff --git a/conda/workflows.go b/conda/workflows.go index 8647e4b9..4a65f565 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -332,9 +332,9 @@ func LegacyEnvironment(force bool, configurations ...string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() - callback := pathlib.LockWaitMessage("Serialized environment creation") + completed := pathlib.LockWaitMessage("Serialized environment creation [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000) - callback() + completed() if err != nil { common.Log("Could not get lock on live environment. Quitting!") return err diff --git a/docs/changelog.md b/docs/changelog.md index 1e4164f3..c9e67cdb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.28.2 (date: 19.10.2022) + +- made lock wait messages little more descriptive and added more of them +- added "pids" folder to keep track who is holding locks (just information) + ## v11.28.1 (date: 12.10.2022) - bugfix: direct initializing robot did not update templates diff --git a/htfs/commands.go b/htfs/commands.go index cc452868..b9d59b91 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -41,9 +41,9 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin common.Progress(1, "Fresh [private mode] holotree environment %v.", xviper.TrackingIdentity()) } - callback := pathlib.LockWaitMessage("Serialized environment creation") + completed := pathlib.LockWaitMessage("Serialized environment creation [holotree lock]") locker, err := pathlib.Locker(common.HolotreeLock(), 30000) - callback() + completed() fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/htfs/directory.go b/htfs/directory.go index 7d936f94..ada5c67c 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -35,6 +35,7 @@ func init() { pathlib.MakeSharedDir(common.HololibCatalogLocation()) pathlib.MakeSharedDir(common.HololibLibraryLocation()) pathlib.MakeSharedDir(common.HololibUsageLocation()) + pathlib.MakeSharedDir(common.HololibPids()) } type Filetask func(string, *File) anywork.Work diff --git a/htfs/library.go b/htfs/library.go index 5582e49a..7e55a97b 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -316,7 +316,9 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) + completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() journal.Post("space-used", metafile, "normal holotree with blueprint %s from %s", key, catalog) diff --git a/htfs/virtual.go b/htfs/virtual.go index 474662e9..076a1b0b 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -80,7 +80,9 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(common.HolotreeLocation(), name) lockfile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) + completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree virtual lock]") locker, err := pathlib.Locker(lockfile, 30000) + completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() journal.Post("space-used", metafile, "virutal holotree with blueprint %s", key) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 872ace79..27d4ab8a 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -91,7 +91,9 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) + completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) defer locker.Release() journal.Post("space-used", metafile, "zipped holotree with blueprint %s from %s", key, catalog) diff --git a/operations/cache.go b/operations/cache.go index 706fba87..f986e4d6 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -64,7 +64,9 @@ func cacheLocation() string { func SummonCache() (*Cache, error) { var result Cache + completed := pathlib.LockWaitMessage("Serialized cache access [cache lock]") locker, err := pathlib.Locker(cacheLockFile(), 125) + completed() if err != nil { return nil, err } @@ -84,7 +86,9 @@ func SummonCache() (*Cache, error) { } func (it *Cache) Save() error { + completed := pathlib.LockWaitMessage("Serialized cache access [cache lock]") locker, err := pathlib.Locker(cacheLockFile(), 125) + completed() if err != nil { return err } diff --git a/pathlib/lock.go b/pathlib/lock.go index bc105817..b868e5da 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -1,7 +1,10 @@ package pathlib import ( + "fmt" "os" + "os/user" + "path/filepath" "time" "github.com/robocorp/rcc/common" @@ -13,6 +16,7 @@ type Releaser interface { type Locked struct { *os.File + Marker string } type fake bool @@ -36,7 +40,7 @@ func waitingLockNotification(message string, latch chan bool) { case <-time.After(delay): counter += 1 delay *= 3 - common.Log("#%d: %s (lock wait)", counter, message) + common.Log("#%d: %s (rcc lock wait warning)", counter, message) common.Timeline("waiting for lock") } } @@ -49,3 +53,15 @@ func LockWaitMessage(message string) func() { latch <- true } } + +func lockPidFilename(lockfile string) string { + now := time.Now().Format("20060102150405") + base := filepath.Base(lockfile) + username := "unspecified" + who, err := user.Current() + if err == nil { + username = who.Username + } + marker := fmt.Sprintf("%s.%s.%s.%s.%d.%s", now, username, common.ControllerType, common.HolotreeSpace, os.Getpid(), base) + return filepath.Join(common.HololibPids(), marker) +} diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 83d5dd6a..305e5ffc 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -6,6 +6,7 @@ package pathlib import ( "os" "syscall" + "time" "github.com/robocorp/rcc/common" ) @@ -34,10 +35,13 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } - return &Locked{file}, nil + marker := lockPidFilename(filename) + ForceTouchWhen(marker, time.Now()) + return &Locked{file, marker}, nil } func (it Locked) Release() error { + defer os.Remove(it.Marker) defer it.Close() err := syscall.Flock(int(it.Fd()), int(syscall.LOCK_UN)) common.Trace("LOCKER: release %v with err: %v", it.Name(), err) diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 6c2f05f6..ffe7124d 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -67,13 +67,16 @@ func Locker(filename string, trycount int) (Releaser, error) { return nil, err } if success { - return &Locked{file}, nil + marker := lockPidFilename(filename) + ForceTouchWhen(marker, time.Now()) + return &Locked{file, marker}, nil } time.Sleep(40 * time.Millisecond) } } func (it Locked) Release() error { + defer os.Remove(it.Marker) success, err := trylock(unlockFile, it) common.Trace("LOCKER: release %v success: %v with err: %v", it.Name(), success, err) return err diff --git a/xviper/wrapper.go b/xviper/wrapper.go index c443f099..ebcaa802 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -38,7 +38,9 @@ func (it *config) Save() { if len(it.Filename) == 0 { return } + completed := pathlib.LockWaitMessage("Serialized config access [config lock]") locker, err := pathlib.Locker(it.Lockfile, 125) + completed() if err != nil { common.Log("FATAL: could not lock %v, reason %v; ignored.", it.Lockfile, err) return @@ -57,7 +59,9 @@ func (it *config) Save() { } func (it *config) Reload() { + completed := pathlib.LockWaitMessage("Serialized config access [config lock]") locker, err := pathlib.Locker(it.Lockfile, 125) + completed() if err != nil { common.Log("FATAL: could not lock %v, reason %v; ignored.", it.Lockfile, err) return From 58986efebdffe594807c1b29336da046e095e1bd Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 Oct 2022 17:07:59 +0300 Subject: [PATCH 308/516] IMPROVEMENT: lock diagnostics warnings (v11.28.3) - added configuration diagnostic reporting on locking pids information --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/diagnostics.go | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 1460a25e..61895af2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.28.2` + Version = `v11.28.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index c9e67cdb..448455c3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.28.3 (date: 19.10.2022) + +- added configuration diagnostic reporting on locking pids information + ## v11.28.2 (date: 19.10.2022) - made lock wait messages little more descriptive and added more of them diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 8e72dc87..4e661a66 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -119,6 +119,7 @@ func RunDiagnostics() *common.DiagnosticStatus { if !common.OverrideSystemRequirements() { result.Checks = append(result.Checks, longPathSupportCheck()) } + result.Checks = append(result.Checks, lockpidsCheck()...) for _, host := range settings.Global.Hostnames() { result.Checks = append(result.Checks, dnsLookupCheck(host)) } @@ -156,6 +157,24 @@ func longPathSupportCheck() *common.DiagnosticCheck { } } +func lockpidsCheck() []*common.DiagnosticCheck { + entries, err := os.ReadDir(common.HololibPids()) + if err != nil { + return []*common.DiagnosticCheck{} + } + support := settings.Global.DocsLink("troubleshooting") + result := make([]*common.DiagnosticCheck, 0, len(entries)) + for _, entry := range entries { + result = append(result, &common.DiagnosticCheck{ + Type: "OS", + Status: statusWarning, + Message: fmt.Sprintf("Pending lock file info: %q", entry.Name()), + Link: support, + }) + } + return result +} + func anyPathCheck(key string) *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") anyPath := os.Getenv(key) From 00986d9bf3d9f09a88987d4a2096e555d54b25c4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 25 Oct 2022 17:07:56 +0300 Subject: [PATCH 309/516] FEATURE: unmanaged holotree spaces (v11.29.0) - started adding support for unmanaged holotree spaces, to enable IT managed holotree spaces (rcc will create them once, but integrity check are not done when unmanaged spaces are used) - bugfix: removing also .lck files when removing space --- cmd/root.go | 1 + common/variables.go | 4 ++ common/version.go | 2 +- docs/changelog.md | 7 +++ htfs/commands.go | 6 ++ htfs/library.go | 18 ++++++ htfs/unmanaged.go | 131 ++++++++++++++++++++++++++++++++++++++++++++ htfs/virtual.go | 9 +++ htfs/ziplibrary.go | 19 +++++++ pretty/variables.go | 2 + 10 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 htfs/unmanaged.go diff --git a/cmd/root.go b/cmd/root.go index 301c1f52..d19df1cf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -118,6 +118,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") } func initConfig() { diff --git a/common/variables.go b/common/variables.go index 1b204421..76a4665b 100644 --- a/common/variables.go +++ b/common/variables.go @@ -28,6 +28,7 @@ var ( NoCache bool NoOutputCapture bool Liveonly bool + UnmanagedSpace bool StageFolder string ControllerType string HolotreeSpace string @@ -271,6 +272,9 @@ func ensureDirectory(name string) { } func UserHomeIdentity() string { + if UnmanagedSpace { + return "UNMNGED" + } location, err := os.UserHomeDir() if err != nil { return "badcafe" diff --git a/common/version.go b/common/version.go index 61895af2..b3514fec 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.28.3` + Version = `v11.29.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 448455c3..0dc4827c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.29.0 (date: 25.10.2022) WIP + +- started adding support for unmanaged holotree spaces, to enable IT managed + holotree spaces (rcc will create them once, but integrity check are not + done when unmanaged spaces are used) +- bugfix: removing also .lck files when removing space + ## v11.28.3 (date: 19.10.2022) - added configuration diagnostic reporting on locking pids information diff --git a/htfs/commands.go b/htfs/commands.go index b9d59b91..91e1dbce 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -59,6 +59,11 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin tree = Virtual() common.Timeline("downgraded to virtual holotree library") } + if common.UnmanagedSpace { + tree = Unmanaged(tree) + } + err = tree.ValidateBlueprint(holotreeBlueprint) + fail.On(err != nil, "%s", err) scorecard = common.NewScorecard() var library Library if haszip { @@ -156,6 +161,7 @@ func RemoveHolotreeSpace(label string) (err error) { continue } TryRemove("metafile", metafile) + TryRemove("lockfile", directory+".lck") err = TryRemoveAll("space", directory) fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) } diff --git a/htfs/library.go b/htfs/library.go index 7e55a97b..918d50ce 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -47,8 +47,10 @@ func (it *stats) Dirty(dirty bool) { type Closer func() error type Library interface { + ValidateBlueprint([]byte) error HasBlueprint([]byte) bool Open(string) (io.Reader, Closer, error) + TargetDir([]byte, []byte, []byte) (string, error) Restore([]byte, []byte, []byte) (string, error) } @@ -216,6 +218,10 @@ func (it *hololib) CatalogPath(key string) string { return filepath.Join(common.HololibCatalogLocation(), CatalogName(key)) } +func (it *hololib) ValidateBlueprint(blueprint []byte) error { + return nil +} + func (it *hololib) HasBlueprint(blueprint []byte) bool { key := BlueprintHash(blueprint) found, ok := it.queryCache[key] @@ -301,6 +307,18 @@ func touchUsedHash(hash string) { pathlib.ForceTouchWhen(fullpath, common.ProgressMark) } +func (it *hololib) TargetDir(blueprint, client, tag []byte) (result string, err error) { + defer fail.Around(&err) + key := BlueprintHash(blueprint) + catalog := it.CatalogPath(key) + fs, err := NewRoot(it.Stage()) + fail.On(err != nil, "Failed to create stage -> %v", err) + err = fs.LoadFrom(catalog) + fail.On(err != nil, "Failed to load catalog %s -> %v", catalog, err) + name := ControllerSpaceName(client, tag) + return filepath.Join(fs.HolotreeBase(), name), nil +} + func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go new file mode 100644 index 00000000..51737553 --- /dev/null +++ b/htfs/unmanaged.go @@ -0,0 +1,131 @@ +package htfs + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" +) + +type unmanaged struct { + delegate MutableLibrary + path string + resolved bool + protected bool +} + +func Unmanaged(core MutableLibrary) MutableLibrary { + return &unmanaged{ + delegate: core, + path: "", + resolved: false, + protected: false, + } +} + +func (it *unmanaged) Identity() string { + return it.delegate.Identity() +} + +func (it *unmanaged) Stage() string { + return it.delegate.Stage() +} + +func (it *unmanaged) CatalogPath(key string) string { + return "Unmanaged Does Not Support Catalog Path Request" +} + +func (it *unmanaged) Remove([]string) error { + return fmt.Errorf("Not supported yet on unmanaged holotree.") +} + +func (it *unmanaged) Export([]string, string) error { + return fmt.Errorf("Not supported yet on unmanaged holotree.") +} + +func (it *unmanaged) resolve(blueprint []byte) error { + if it.resolved { + return nil + } + controller := []byte(common.ControllerIdentity()) + space := []byte(common.HolotreeSpace) + path, err := it.TargetDir(blueprint, controller, space) + if err != nil { + return nil + } + if !pathlib.Exists(path) { + it.path = path + it.resolved = true + return nil + } + identityfile := filepath.Join(path, "identity.yaml") + _, identity, err := ComposeFinalBlueprint([]string{identityfile}, "") + if err != nil { + return nil + } + expected := BlueprintHash(blueprint) + actual := BlueprintHash(identity) + if actual != expected { + return fmt.Errorf("Unmanaged fingerprint %q does not match requested one %q! Quitting!", actual, expected) + } + it.path = path + it.protected = true + it.resolved = true + return nil +} + +func (it *unmanaged) ValidateBlueprint(blueprint []byte) error { + err := it.resolve(blueprint) + if err != nil { + return err + } + if it.protected { + return nil + } + return it.delegate.ValidateBlueprint(blueprint) +} + +func (it *unmanaged) Record(blueprint []byte) error { + it.resolve(blueprint) + if it.protected { + common.Timeline("holotree unmanaged record prevention") + return nil + } + return it.delegate.Record(blueprint) +} + +func (it *unmanaged) TargetDir(blueprint, client, tag []byte) (string, error) { + return it.delegate.TargetDir(blueprint, client, tag) +} + +func (it *unmanaged) Restore(blueprint, client, tag []byte) (string, error) { + defer common.Log("%sThis is unmanaged holotree space for blueprint: %v%s", pretty.Magenta, BlueprintHash(blueprint), pretty.Reset) + it.resolve(blueprint) + if !it.protected { + return it.delegate.Restore(blueprint, client, tag) + } + common.Timeline("holotree unmanaged restore prevention") + if len(it.path) > 0 { + return it.path, nil + } + return "", fmt.Errorf("Unmanaged path resolution failed!") +} + +func (it *unmanaged) Open(digest string) (readable io.Reader, closer Closer, err error) { + return it.delegate.Open(digest) +} + +func (it *unmanaged) ExactLocation(key string) string { + return it.delegate.ExactLocation(key) +} + +func (it *unmanaged) Location(key string) string { + return it.delegate.Location(key) +} + +func (it *unmanaged) HasBlueprint(blueprint []byte) bool { + return it.delegate.HasBlueprint(blueprint) +} diff --git a/htfs/virtual.go b/htfs/virtual.go index 076a1b0b..8ef7e4de 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -72,6 +72,11 @@ func (it *virtual) Record(blueprint []byte) (err error) { return nil } +func (it *virtual) TargetDir(blueprint, client, tag []byte) (string, error) { + name := ControllerSpaceName(client, tag) + return filepath.Join(common.HolotreeLocation(), name), nil +} + func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) @@ -137,6 +142,10 @@ func (it *virtual) Location(key string) string { panic("Location is not supported on virtual holotree.") } +func (it *virtual) ValidateBlueprint(blueprint []byte) error { + return nil +} + func (it *virtual) HasBlueprint(blueprint []byte) bool { return it.key == BlueprintHash(blueprint) } diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 27d4ab8a..1897fd56 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -39,6 +39,10 @@ func ZipLibrary(zipfile string) (Library, error) { }, nil } +func (it *ziplibrary) ValidateBlueprint(blueprint []byte) error { + return nil +} + func (it *ziplibrary) HasBlueprint(blueprint []byte) bool { key := BlueprintHash(blueprint) _, ok := it.lookup[it.CatalogPath(key)] @@ -74,6 +78,21 @@ func (it *ziplibrary) CatalogPath(key string) string { return filepath.Join("catalog", CatalogName(key)) } +func (it *ziplibrary) TargetDir(blueprint, client, tag []byte) (path string, err error) { + defer fail.Around(&err) + key := BlueprintHash(blueprint) + name := ControllerSpaceName(client, tag) + fs, err := NewRoot(".") + fail.On(err != nil, "Failed to create root -> %v", err) + catalog := it.CatalogPath(key) + reader, closer, err := it.openFile(catalog) + fail.On(err != nil, "Failed to open catalog %q -> %v", catalog, err) + defer closer() + err = fs.ReadFrom(reader) + fail.On(err != nil, "Failed to read catalog %q -> %v", catalog, err) + return filepath.Join(fs.HolotreeBase(), name), nil +} + func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() diff --git a/pretty/variables.go b/pretty/variables.go index 36c67fee..1e302ad0 100644 --- a/pretty/variables.go +++ b/pretty/variables.go @@ -18,6 +18,7 @@ var ( Red string Green string Yellow string + Magenta string Cyan string Reset string Sparkles string @@ -45,6 +46,7 @@ func Setup() { Black = csi("30m") Red = csi("91m") Green = csi("92m") + Magenta = csi("95m") Cyan = csi("96m") Yellow = csi("93m") Reset = csi("0m") From 202827cc1681684ea1ba0987f0ebe897bfccf504 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 26 Oct 2022 12:41:48 +0300 Subject: [PATCH 310/516] BUGFIX: unmanaged space detection (v11.29.1) - robot tests for unmanaged holotree spaces (revealed bugs) - bugfix: correct checking of unmanaged space conflicts (on creation) --- common/version.go | 2 +- docs/changelog.md | 7 +- htfs/library.go | 8 +- htfs/unmanaged.go | 7 +- robot_tests/export_holozip.robot | 1 - robot_tests/python375.yaml | 7 ++ robot_tests/python3913.yaml | 7 ++ robot_tests/unmanaged_space.robot | 133 ++++++++++++++++++++++++++++++ 8 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 robot_tests/python375.yaml create mode 100644 robot_tests/python3913.yaml create mode 100644 robot_tests/unmanaged_space.robot diff --git a/common/version.go b/common/version.go index b3514fec..6fae7d01 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.29.0` + Version = `v11.29.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0dc4827c..1d002640 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # rcc change log -## v11.29.0 (date: 25.10.2022) WIP +## v11.29.1 (date: 26.10.2022) UNSTABLE + +- robot tests for unmanaged holotree spaces (revealed bugs) +- bugfix: correct checking of unmanaged space conflicts (on creation) + +## v11.29.0 (date: 25.10.2022) BROKEN - started adding support for unmanaged holotree spaces, to enable IT managed holotree spaces (rcc will create them once, but integrity check are not diff --git a/htfs/library.go b/htfs/library.go index 918d50ce..50ea34f3 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -307,15 +307,17 @@ func touchUsedHash(hash string) { pathlib.ForceTouchWhen(fullpath, common.ProgressMark) } -func (it *hololib) TargetDir(blueprint, client, tag []byte) (result string, err error) { +func (it *hololib) TargetDir(blueprint, controller, space []byte) (result string, err error) { defer fail.Around(&err) key := BlueprintHash(blueprint) catalog := it.CatalogPath(key) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) + name := ControllerSpaceName(controller, space) err = fs.LoadFrom(catalog) - fail.On(err != nil, "Failed to load catalog %s -> %v", catalog, err) - name := ControllerSpaceName(client, tag) + if err != nil { + return filepath.Join(common.HolotreeLocation(), name), nil + } return filepath.Join(fs.HolotreeBase(), name), nil } diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index 51737553..e2869c0e 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -50,10 +50,12 @@ func (it *unmanaged) resolve(blueprint []byte) error { if it.resolved { return nil } + defer common.Log("%sThis is unmanaged holotree space, checking suitability for blueprint: %v%s", pretty.Magenta, BlueprintHash(blueprint), pretty.Reset) controller := []byte(common.ControllerIdentity()) space := []byte(common.HolotreeSpace) path, err := it.TargetDir(blueprint, controller, space) if err != nil { + common.Debug("Unmanaged target directory error: %v (path: %q)", err, path) return nil } if !pathlib.Exists(path) { @@ -69,7 +71,9 @@ func (it *unmanaged) resolve(blueprint []byte) error { expected := BlueprintHash(blueprint) actual := BlueprintHash(identity) if actual != expected { - return fmt.Errorf("Unmanaged fingerprint %q does not match requested one %q! Quitting!", actual, expected) + it.protected = true + it.resolved = true + return fmt.Errorf("Existing unmanaged space fingerprint %q does not match requested one %q! Quitting!", actual, expected) } it.path = path it.protected = true @@ -102,7 +106,6 @@ func (it *unmanaged) TargetDir(blueprint, client, tag []byte) (string, error) { } func (it *unmanaged) Restore(blueprint, client, tag []byte) (string, error) { - defer common.Log("%sThis is unmanaged holotree space for blueprint: %v%s", pretty.Magenta, BlueprintHash(blueprint), pretty.Reset) it.resolve(blueprint) if !it.protected { return it.delegate.Restore(blueprint, client, tag) diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 3dd321f7..8eb2bff7 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -4,7 +4,6 @@ Library supporting.py Resource resources.robot Suite Setup Export setup Suite Teardown Export teardown -Default tags WIP *** Keywords *** Export setup diff --git a/robot_tests/python375.yaml b/robot_tests/python375.yaml new file mode 100644 index 00000000..514f9dd5 --- /dev/null +++ b/robot_tests/python375.yaml @@ -0,0 +1,7 @@ +channels: +- conda-forge +dependencies: +- python==3.7.5 +- pip==20.1 +- pip: + - requests==2.28.1 diff --git a/robot_tests/python3913.yaml b/robot_tests/python3913.yaml new file mode 100644 index 00000000..3196015f --- /dev/null +++ b/robot_tests/python3913.yaml @@ -0,0 +1,7 @@ +channels: +- conda-forge +dependencies: +- python==3.9.13 +- pip==22.2.2 +- pip: + - requests==2.28.1 diff --git a/robot_tests/unmanaged_space.robot b/robot_tests/unmanaged_space.robot new file mode 100644 index 00000000..611fa7ac --- /dev/null +++ b/robot_tests/unmanaged_space.robot @@ -0,0 +1,133 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot +Suite Setup Holotree setup +Default tags WIP + +*** Keywords *** +Holotree setup + Fire And Forget build/rcc ht delete 4e67cd8 + +*** Test cases *** + +Goal: See variables from specific unamanged space + Step build/rcc holotree variables --unmanaged --space python39 --controller citests robot_tests/python3913.yaml + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHONPATH= + Wont Have ROBOT_ROOT= + Wont Have ROBOT_ARTIFACTS= + Use STDERR + Must Have This is unmanaged holotree space + Must Have Progress: 01/13 + Must Have Progress: 02/13 + Must Have Progress: 04/13 + Must Have Progress: 05/13 + Must Have Progress: 06/13 + Must Have Progress: 12/13 + Must Have Progress: 13/13 + +Goal: Wont allow use of unmanaged space with incompatible conda.yaml + Step build/rcc holotree variables --debug --unmanaged --space python39 --controller citests robot_tests/python375.yaml 6 + Wont Have ROBOCORP_HOME= + Wont Have PYTHON_EXE= + Wont Have RCC_ENVIRONMENT_HASH= + Wont Have RCC_INSTALLATION_ID= + Use STDERR + Must Have This is unmanaged holotree space + Must Have Progress: 01/13 + Must Have Progress: 02/13 + Must Have Progress: 13/13 + + Wont Have Progress: 04/13 + Wont Have Progress: 05/13 + Wont Have Progress: 06/13 + Wont Have Progress: 12/13 + + Must Have Existing unmanaged space fingerprint + Must Have does not match requested one + Must Have Quitting! + +Goal: Allows different unmanaged space for different conda.yaml + Step build/rcc holotree variables --unmanaged --space python37 --controller citests robot_tests/python375.yaml + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Wont Have PYTHONPATH= + Wont Have ROBOT_ROOT= + Wont Have ROBOT_ARTIFACTS= + Use STDERR + Must Have This is unmanaged holotree space + Must Have Progress: 01/13 + Must Have Progress: 02/13 + Must Have Progress: 04/13 + Must Have Progress: 05/13 + Must Have Progress: 06/13 + Must Have Progress: 12/13 + Must Have Progress: 13/13 + +Goal: Wont allow use of unmanaged space with incompatible conda.yaml when two unmanaged spaces exists + Step build/rcc holotree variables --debug --unmanaged --space python37 --controller citests robot_tests/python3913.yaml 6 + Use STDERR + Must Have This is unmanaged holotree space + Must Have Progress: 01/13 + Must Have Progress: 02/13 + Must Have Progress: 13/13 + + Wont Have Progress: 05/13 + Wont Have Progress: 12/13 + + Must Have Existing unmanaged space fingerprint + Must Have does not match requested one + Must Have Quitting! + +Goal: See variables from specific environment without robot.yaml knowledge in JSON form + Step build/rcc holotree variables --unmanaged --space python39 --controller citests --json robot_tests/python3913.yaml + Must Be Json Response + Use STDERR + Must Have This is unmanaged holotree space + +Goal: Can see unmanaged spaces in listings + Step build/rcc holotree list --controller citests + Use STDERR + Must Have UNMNGED_ + Must Have python37 + Must Have python39 + +Goal: Can delete all unmanaged spaces with one command + Step build/rcc holotree delete --controller citests UNMNGED_ + Use STDERR + Must Have Removing UNMNGED_ + +Goal: After deleted, cannot see unmanaged spaces in listings + Step build/rcc holotree list --controller citests + Use STDERR + Wont Have UNMNGED_ + Wont Have python37 + Wont Have python39 From d40e6aea5099af918b48b9f0edf727796d3d570f Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 27 Oct 2022 15:38:52 +0300 Subject: [PATCH 311/516] Update robocorp_stack.png --- docs/robocorp_stack.png | Bin 101938 -> 66899 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/robocorp_stack.png b/docs/robocorp_stack.png index 8fbeeab63f71e710de2073e5fdac0eeb8074146e..9d5fac4457c0dc7ccda3118af6c30cba470b2cb4 100644 GIT binary patch literal 66899 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 z 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 F{{qrh*OUMN From 2914cffb131ad99751716e724b644d9721fe6135 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 2 Nov 2022 13:06:10 +0200 Subject: [PATCH 312/516] FEATURE: openssl vulnerability warning (v11.30.0) - added warning when vulnerable openssl is installed in environment --- common/version.go | 2 +- conda/dependencies.go | 15 +++++++++++++++ docs/changelog.md | 4 ++++ htfs/commands.go | 10 +++++++++- pretty/functions.go | 5 +++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 6fae7d01..6d4ec210 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.29.1` + Version = `v11.30.0` ) diff --git a/conda/dependencies.go b/conda/dependencies.go index 03749b49..cd1c7e83 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -39,6 +39,21 @@ func (it dependencies) sorted() dependencies { return it } +func (it dependencies) WarnVulnerability(url, severity, name string, versions ...string) { + found, ok := it.Lookup(name, false) + if !ok { + found, ok = it.Lookup(name, true) + } + if !ok { + return + } + for _, version := range versions { + if found.Version == version { + pretty.Highlight("Dependency with %s severity vulnerability detected: %s %s. For more information see %s", severity, name, found.Version, url) + } + } +} + func (it dependencies) Lookup(name string, pypi bool) (*dependency, bool) { for _, entry := range it { if pypi && entry.Origin != "pypi" { diff --git a/docs/changelog.md b/docs/changelog.md index 1d002640..49ac0b3f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.30.0 (date: 2.11.2022) + +- added warning when vulnerable openssl is installed in environment + ## v11.29.1 (date: 26.10.2022) UNSTABLE - robot tests for unmanaged holotree spaces (revealed bugs) diff --git a/htfs/commands.go b/htfs/commands.go index 91e1dbce..b49891b8 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -29,11 +29,20 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin common.Debug("New zipped environment from %q!", holozip) } + path := "" defer func() { common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) } + if len(path) > 0 { + dependencies := conda.LoadWantedDependencies(conda.GoldenMasterFilename(path)) + dependencies.WarnVulnerability( + "https://robocorp.com/docs/faq/openssl-cve-2022-11-01", + "HIGH", + "openssl", + "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6") + } }() if common.SharedHolotree { common.Progress(1, "Fresh [shared mode] holotree environment %v.", xviper.TrackingIdentity()) @@ -77,7 +86,6 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin library = tree } - path := "" if restore { common.Progress(12, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) diff --git a/pretty/functions.go b/pretty/functions.go index 55b9f4dd..1764f99a 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -21,6 +21,11 @@ func Warning(format string, rest ...interface{}) { common.Log(niceform, rest...) } +func Highlight(format string, rest ...interface{}) { + niceform := fmt.Sprintf("%s%s%s", Magenta, format, Reset) + common.Log(niceform, rest...) +} + func Exit(code int, format string, rest ...interface{}) { var niceform string if code == 0 { From fb8b905e8cc27aa6983a109284425e73281988db Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 7 Nov 2022 08:15:04 +0200 Subject: [PATCH 313/516] BUGFIX: shared holotree checks (v11.30.1) - bugfix: added more checks around shared holotree enabling and using - bugfix: make all lockfiles readable and writable by all - added "diagnostics" command to toplevel commands --- cmd/diagnostics.go | 2 ++ cmd/holotreeShared.go | 2 +- common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/library.go | 5 +++++ operations/diagnostics.go | 45 +++++++++++++++++++++++++++++++++++++++ pathlib/lock_unix.go | 6 +++++- pathlib/lock_windows.go | 6 +++++- xviper/wrapper.go | 8 +++++++ 9 files changed, 78 insertions(+), 4 deletions(-) diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 66c151d3..9d3da2cd 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -32,6 +32,8 @@ var diagnosticsCmd = &cobra.Command{ func init() { configureCmd.AddCommand(diagnosticsCmd) + rootCmd.AddCommand(diagnosticsCmd) + diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") diagnosticsCmd.Flags().StringVarP(&fileOption, "file", "f", "", "Save output into a file.") diagnosticsCmd.Flags().StringVarP(&robotOption, "robot", "r", "", "Full path to 'robot.yaml' configuration file. [optional]") diff --git a/cmd/holotreeShared.go b/cmd/holotreeShared.go index d1774be5..29409b28 100644 --- a/cmd/holotreeShared.go +++ b/cmd/holotreeShared.go @@ -30,7 +30,7 @@ var holotreeSharedCommand = &cobra.Command{ return } if os.Geteuid() > 0 { - pretty.Warning("Running this command might need sudo/root access rights. Still, trying ...") + pretty.Warning("Running this command might need sudo/root/elevated access rights. Still, trying ...") } osSpecificHolotreeSharing(enableShared) pretty.Ok() diff --git a/common/version.go b/common/version.go index 6d4ec210..8179b2ea 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.30.0` + Version = `v11.30.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 49ac0b3f..6cc74a43 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.30.1 (date: 7.11.2022) + +- bugfix: added more checks around shared holotree enabling and using +- bugfix: make all lockfiles readable and writable by all +- added "diagnostics" command to toplevel commands + ## v11.30.0 (date: 2.11.2022) - added warning when vulnerable openssl is installed in environment diff --git a/htfs/library.go b/htfs/library.go index 50ea34f3..8f8a5112 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -321,6 +321,11 @@ func (it *hololib) TargetDir(blueprint, controller, space []byte) (result string return filepath.Join(fs.HolotreeBase(), name), nil } +func UserHolotreeLockfile() string { + name := ControllerSpaceName([]byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + return filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) +} + func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 4e661a66..790c842d 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -17,6 +17,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -99,6 +100,10 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") result.Details["no-build"] = fmt.Sprintf("%v", settings.Global.NoBuild()) + for name, filename := range lockfiles() { + result.Details[name] = filename + } + who, err := user.Current() if err == nil { result.Details["uid:gid"] = fmt.Sprintf("%s:%s", who.Uid, who.Gid) @@ -120,6 +125,7 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Checks = append(result.Checks, longPathSupportCheck()) } result.Checks = append(result.Checks, lockpidsCheck()...) + result.Checks = append(result.Checks, lockfilesCheck()...) for _, host := range settings.Global.Hostnames() { result.Checks = append(result.Checks, dnsLookupCheck(host)) } @@ -129,6 +135,16 @@ func RunDiagnostics() *common.DiagnosticStatus { return result } +func lockfiles() map[string]string { + result := make(map[string]string) + result["lock-config"] = xviper.Lockfile() + result["lock-cache"] = cacheLockFile() + result["lock-holotree"] = common.HolotreeLock() + result["lock-robocorp"] = common.RobocorpLock() + result["lock-userlock"] = htfs.UserHolotreeLockfile() + return result +} + func rccStatusLine() string { requests := xviper.GetInt("stats.env.request") hits := xviper.GetInt("stats.env.hit") @@ -157,6 +173,35 @@ func longPathSupportCheck() *common.DiagnosticCheck { } } +func lockfilesCheck() []*common.DiagnosticCheck { + content := []byte(fmt.Sprintf("lock check %s @%d", common.Version, common.When)) + files := lockfiles() + result := make([]*common.DiagnosticCheck, 0, len(files)) + support := settings.Global.DocsLink("troubleshooting") + failed := false + for identity, filename := range files { + err := os.WriteFile(filename, content, 0o666) + if err != nil { + result = append(result, &common.DiagnosticCheck{ + Type: "OS", + Status: statusFail, + Message: fmt.Sprintf("Lock file %q write failed, reason: %v", identity, err), + Link: support, + }) + failed = true + } + } + if !failed { + result = append(result, &common.DiagnosticCheck{ + Type: "OS", + Status: statusOk, + Message: fmt.Sprintf("%d lockfiles all seem to work correctly (for this user).", len(files)), + Link: support, + }) + } + return result +} + func lockpidsCheck() []*common.DiagnosticCheck { entries, err := os.ReadDir(common.HololibPids()) if err != nil { diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 305e5ffc..8ac2c48c 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -23,7 +23,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } - file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) if err != nil { return nil, err } @@ -36,6 +36,10 @@ func Locker(filename string, trycount int) (Releaser, error) { return nil, err } marker := lockPidFilename(filename) + _, err = file.Write([]byte(marker)) + if err != nil { + return nil, err + } ForceTouchWhen(marker, time.Now()) return &Locked{file, marker}, nil } diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index ffe7124d..e44501a6 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -46,7 +46,7 @@ func Locker(filename string, trycount int) (Releaser, error) { } for { trycount -= 1 - file, err = os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + file, err = os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) if err != nil && trycount < 0 { return nil, err } @@ -68,6 +68,10 @@ func Locker(filename string, trycount int) (Releaser, error) { } if success { marker := lockPidFilename(filename) + _, err = file.Write([]byte(marker)) + if err != nil { + return nil, err + } ForceTouchWhen(marker, time.Now()) return &Locked{file, marker}, nil } diff --git a/xviper/wrapper.go b/xviper/wrapper.go index ebcaa802..c2f7c6ce 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -132,6 +132,14 @@ func Set(key string, value interface{}) { <-flow } +func Lockfile() string { + flow := make(chan string) + pipeline <- func(core *config) { + flow <- core.Lockfile + } + return <-flow +} + func ConfigFileUsed() string { flow := make(chan string) pipeline <- func(core *config) { From 4481337cb9703b7ec619b7e5a28986af86ed0e52 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 7 Nov 2022 11:41:10 +0200 Subject: [PATCH 314/516] UPGRADE: micromamba upgrade to v1.0.0 (v11.31.0) --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 8179b2ea..73f1cef1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.30.1` + Version = `v11.31.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index cd9a4f46..6a20c496 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - micromambaVersionLimit = 27000 - micromambaVersionNumber = "v0.27.0" + micromambaVersionLimit = 1000000 + micromambaVersionNumber = "v1.0.0" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index 6cc74a43..cc9fa709 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.31.0 (date: 7.11.2022) + +- micromamba upgrade to v1.0.0 + ## v11.30.1 (date: 7.11.2022) - bugfix: added more checks around shared holotree enabling and using From 650ac459df3b8cc7b6b622be28269822b18da392 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Nov 2022 12:25:34 +0200 Subject: [PATCH 315/516] BUGFIX: lock pid files name change and debug info (v11.31.1) - bugfix: changed lock pid filename not to contain extra dots - added more info on pending lock files diagnostics check - more debug information on Windows locking behaviour --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 20 +++++++++++++++++--- pathlib/lock.go | 2 +- pathlib/lock_unix.go | 2 ++ pathlib/lock_windows.go | 2 ++ pathlib/touch.go | 12 ++++++++++-- 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index 73f1cef1..751c12e9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.31.0` + Version = `v11.31.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index cc9fa709..d3d1b4e6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.31.1 (date: 8.11.2022) + +- bugfix: changed lock pid filename not to contain extra dots +- added more info on pending lock files diagnostics check +- more debug information on Windows locking behaviour + ## v11.31.0 (date: 7.11.2022) - micromamba upgrade to v1.0.0 diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 790c842d..2cf0f79f 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -203,12 +203,18 @@ func lockfilesCheck() []*common.DiagnosticCheck { } func lockpidsCheck() []*common.DiagnosticCheck { + support := settings.Global.DocsLink("troubleshooting") + result := []*common.DiagnosticCheck{} entries, err := os.ReadDir(common.HololibPids()) if err != nil { - return []*common.DiagnosticCheck{} + result = append(result, &common.DiagnosticCheck{ + Type: "OS", + Status: statusWarning, + Message: fmt.Sprintf("Problem with pids directory: %q, reason: %v", common.HololibPids(), err), + Link: support, + }) + return result } - support := settings.Global.DocsLink("troubleshooting") - result := make([]*common.DiagnosticCheck, 0, len(entries)) for _, entry := range entries { result = append(result, &common.DiagnosticCheck{ Type: "OS", @@ -217,6 +223,14 @@ func lockpidsCheck() []*common.DiagnosticCheck { Link: support, }) } + if len(result) == 0 { + result = append(result, &common.DiagnosticCheck{ + Type: "OS", + Status: statusOk, + Message: "No pending lock files detected.", + Link: support, + }) + } return result } diff --git a/pathlib/lock.go b/pathlib/lock.go index b868e5da..dbfc4a11 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -62,6 +62,6 @@ func lockPidFilename(lockfile string) string { if err == nil { username = who.Username } - marker := fmt.Sprintf("%s.%s.%s.%s.%d.%s", now, username, common.ControllerType, common.HolotreeSpace, os.Getpid(), base) + marker := fmt.Sprintf("%s_%s_%s_%s_%d_%s", now, username, common.ControllerType, common.HolotreeSpace, os.Getpid(), base) return filepath.Join(common.HololibPids(), marker) } diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 8ac2c48c..ab5e0d17 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -40,12 +40,14 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } + common.Debug("LOCKER: make marker %v", marker) ForceTouchWhen(marker, time.Now()) return &Locked{file, marker}, nil } func (it Locked) Release() error { defer os.Remove(it.Marker) + defer common.Debug("LOCKER: remove marker %v", it.Marker) defer it.Close() err := syscall.Flock(int(it.Fd()), int(syscall.LOCK_UN)) common.Trace("LOCKER: release %v with err: %v", it.Name(), err) diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index e44501a6..2b18ec58 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -72,6 +72,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } + common.Debug("LOCKER: make marker %v", marker) ForceTouchWhen(marker, time.Now()) return &Locked{file, marker}, nil } @@ -81,6 +82,7 @@ func Locker(filename string, trycount int) (Releaser, error) { func (it Locked) Release() error { defer os.Remove(it.Marker) + defer common.Debug("LOCKER: remove marker %v", it.Marker) success, err := trylock(unlockFile, it) common.Trace("LOCKER: release %v success: %v with err: %v", it.Name(), success, err) return err diff --git a/pathlib/touch.go b/pathlib/touch.go index dc7d0c1a..71d53e29 100644 --- a/pathlib/touch.go +++ b/pathlib/touch.go @@ -3,15 +3,23 @@ package pathlib import ( "os" "time" + + "github.com/robocorp/rcc/common" ) func TouchWhen(location string, when time.Time) { - os.Chtimes(location, when, when) + err := os.Chtimes(location, when, when) + if err != nil { + common.Debug("Touching file %q failed, reason: %v ... ignored!", location, err) + } } func ForceTouchWhen(location string, when time.Time) { if !Exists(location) { - os.WriteFile(location, []byte{}, 0o644) + err := os.WriteFile(location, []byte{}, 0o644) + if err != nil { + common.Debug("Touch/creating file %q failed, reason: %v ... ignored!", location, err) + } } TouchWhen(location, when) } From 65cb4b6f9fb30cffd61c3f0d3617d33b51390c61 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Nov 2022 13:53:04 +0200 Subject: [PATCH 316/516] BUGFIX: lock pid files name fix (v11.31.2) - bugfix: removing path separators from user name on lock pid files --- common/version.go | 2 +- docs/changelog.md | 4 ++++ pathlib/lock.go | 13 ++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 751c12e9..cff31577 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.31.1` + Version = `v11.31.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index d3d1b4e6..cdc98f07 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.31.2 (date: 8.11.2022) + +- bugfix: removing path separators from user name on lock pid files + ## v11.31.1 (date: 8.11.2022) - bugfix: changed lock pid filename not to contain extra dots diff --git a/pathlib/lock.go b/pathlib/lock.go index dbfc4a11..177a1334 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -5,11 +5,17 @@ import ( "os" "os/user" "path/filepath" + "regexp" + "strings" "time" "github.com/robocorp/rcc/common" ) +var ( + slashPattern = regexp.MustCompile("[/\\\\]+") +) + type Releaser interface { Release() error } @@ -54,13 +60,18 @@ func LockWaitMessage(message string) func() { } } +func unslash(text string) string { + parts := slashPattern.Split(text, -1) + return strings.Join(parts, "_") +} + func lockPidFilename(lockfile string) string { now := time.Now().Format("20060102150405") base := filepath.Base(lockfile) username := "unspecified" who, err := user.Current() if err == nil { - username = who.Username + username = unslash(who.Username) } marker := fmt.Sprintf("%s_%s_%s_%s_%d_%s", now, username, common.ControllerType, common.HolotreeSpace, os.Getpid(), base) return filepath.Join(common.HololibPids(), marker) From 88df57bdb10ca3fd89c448cb946e02433aa012b0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Nov 2022 09:46:25 +0200 Subject: [PATCH 317/516] FEATURE: holotree build statistics (v11.32.0) - feature: local recording of holotree environment build statistics events - moved journals to `ROBOCORP_HOME/journals` directory (and build stats will be part of those journals) - added pre run scripts to timeline --- cmd/assistantRun.go | 2 + cmd/cloudPrepare.go | 2 + cmd/holotreeVariables.go | 2 + cmd/run.go | 2 + common/elapsed.go | 12 +++ common/variables.go | 13 +++- common/version.go | 2 +- conda/cleanup.go | 4 + conda/workflows.go | 6 ++ docs/changelog.md | 7 ++ htfs/commands.go | 7 ++ htfs/library.go | 6 ++ htfs/virtual.go | 1 + htfs/ziplibrary.go | 1 + journal/buildstats.go | 155 +++++++++++++++++++++++++++++++++++++++ journal/journal.go | 12 +-- operations/running.go | 6 ++ 17 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 journal/buildstats.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 8bcb5d4a..ce02d3a1 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -25,6 +26,7 @@ var assistantRunCmd = &cobra.Command{ Long: "Robot Assistant run.", Run: func(cmd *cobra.Command, args []string) { defer conda.RemoveCurrentTemp() + defer journal.BuildEventStats("assistant") var status, reason string status, reason = "ERROR", "UNKNOWN" elapser := common.Stopwatch("Robot Assistant startup lasted") diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index c331b60a..c073eccc 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -21,6 +22,7 @@ var prepareCloudCmd = &cobra.Command{ Short: "Prepare cloud robot for fast startup time in local computer.", Long: "Prepare cloud robot for fast startup time in local computer.", Run: func(cmd *cobra.Command, args []string) { + defer journal.BuildEventStats("prepare") if common.DebugFlag { defer common.Stopwatch("Cloud prepare lasted").Report() } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 19a21cfc..65883408 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -125,6 +126,7 @@ var holotreeVariablesCmd = &cobra.Command{ Short: "Do holotree operations.", Long: "Do holotree operations.", Run: func(cmd *cobra.Command, args []string) { + defer journal.BuildEventStats("variables") if common.DebugFlag { defer common.Stopwatch("Holotree variables command lasted").Report() } diff --git a/cmd/run.go b/cmd/run.go index dfd1141b..19856fcb 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -4,6 +4,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/xviper" @@ -24,6 +25,7 @@ var runCmd = &cobra.Command{ in your own machine.`, Run: func(cmd *cobra.Command, args []string) { defer conda.RemoveCurrentTemp() + defer journal.BuildEventStats("robot") if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() } diff --git a/common/elapsed.go b/common/elapsed.go index 63ebaad3..e570d71b 100644 --- a/common/elapsed.go +++ b/common/elapsed.go @@ -12,6 +12,10 @@ type stopwatch struct { type Duration time.Duration +func (it Duration) Seconds() float64 { + return float64(it.Truncate(time.Millisecond)) / float64(time.Second) +} + func (it Duration) Truncate(granularity time.Duration) Duration { return Duration(time.Duration(it).Truncate(granularity)) } @@ -32,6 +36,14 @@ func Stopwatch(form string, details ...interface{}) *stopwatch { } } +func (it *stopwatch) When() int64 { + return it.started.Unix() +} + +func (it *stopwatch) Time() time.Time { + return it.started +} + func (it *stopwatch) String() string { elapsed := it.Elapsed().Truncate(time.Millisecond) return fmt.Sprintf("%v", elapsed) diff --git a/common/variables.go b/common/variables.go index 76a4665b..6019e730 100644 --- a/common/variables.go +++ b/common/variables.go @@ -43,7 +43,7 @@ var ( func init() { Clock = &stopwatch{"Clock", time.Now()} - When = Clock.started.Unix() + When = Clock.When() ProgressMark = time.Now() randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) @@ -55,6 +55,7 @@ func init() { SharedHolotree = isFile(HoloInitUserFile()) + ensureDirectory(JournalLocation()) ensureDirectory(TemplateLocation()) ensureDirectory(BinLocation()) ensureDirectory(PipCache()) @@ -94,10 +95,18 @@ func BinRcc() string { return self } -func EventJournal() string { +func OldEventJournal() string { return filepath.Join(RobocorpHome(), "event.log") } +func EventJournal() string { + return filepath.Join(JournalLocation(), "event.log") +} + +func JournalLocation() string { + return filepath.Join(RobocorpHome(), "journals") +} + func TemplateLocation() string { return filepath.Join(RobocorpHome(), "templates") } diff --git a/common/version.go b/common/version.go index cff31577..811908c9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.31.2` + Version = `v11.32.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 9ad45a45..3c31f7d4 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -91,12 +91,16 @@ func spotlessCleanup(dryrun bool) error { } if dryrun { common.Log("- %v", BinMicromamba()) + common.Log("- %v", common.OldEventJournal()) common.Log("- %v", common.RobotCache()) common.Log("- %v", common.HololibLocation()) + common.Log("- %v", common.JournalLocation()) return nil } safeRemove("executable", BinMicromamba()) safeRemove("cache", common.RobotCache()) + safeRemove("old", common.OldEventJournal()) + safeRemove("journals", common.JournalLocation()) return safeRemove("cache", common.HololibLocation()) } diff --git a/conda/workflows.go b/conda/workflows.go index 4a65f565..76c3f570 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -13,6 +13,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" @@ -103,6 +104,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall common.Timeline("first try.") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { + journal.CurrentBuildEvent().Rebuild() cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) common.Debug("=== second try phase ===") common.Timeline("second try.") @@ -114,6 +116,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall } success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, postInstall) } + journal.CurrentBuildEvent().Successful() return success, nil } @@ -164,6 +167,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Fatal(fmt.Sprintf("Micromamba [%d/%x]", code, code), err) return false, false } + journal.CurrentBuildEvent().MicromambaComplete() common.Timeline("micromamba done.") if observer.HasFailures(targetFolder) { return false, true @@ -199,6 +203,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Fatal(fmt.Sprintf("Pip [%d/%x]", code, code), err) return false, false } + journal.CurrentBuildEvent().PipComplete() common.Timeline("pip done.") pipUsed = true } @@ -222,6 +227,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } } + journal.CurrentBuildEvent().PostInstallComplete() } else { common.Progress(7, "Post install scripts phase skipped -- no scripts.") } diff --git a/docs/changelog.md b/docs/changelog.md index cdc98f07..e0e6d11f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.32.0 (date: 10.11.2022) UNSTABLE + +- feature: local recording of holotree environment build statistics events +- moved journals to `ROBOCORP_HOME/journals` directory (and build stats will + be part of those journals) +- added pre run scripts to timeline + ## v11.31.2 (date: 8.11.2022) - bugfix: removing path separators from user name on lock pid files diff --git a/htfs/commands.go b/htfs/commands.go index b49891b8..fe6b656d 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -20,6 +21,8 @@ import ( func NewEnvironment(condafile, holozip string, restore, force bool) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) + journal.CurrentBuildEvent().StartNow(force) + if settings.Global.NoBuild() { pretty.Note("'no-build' setting is active. Only cached, prebuild, or imported environments are allowed!") } @@ -60,6 +63,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin fail.On(err != nil, "%s", err) common.EnvironmentHash = BlueprintHash(holotreeBlueprint) common.Progress(2, "Holotree blueprint is %q [%s].", common.EnvironmentHash, common.Platform()) + journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() fail.On(err != nil, "%s", err) @@ -90,6 +94,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin common.Progress(12, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + journal.CurrentBuildEvent().RestoreComplete() } else { common.Progress(12, "Restoring space skipped.") } @@ -123,6 +128,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec fail.On(settings.Global.NoBuild(), "Building new holotree environment is blocked by settings, and could not be found from hololib cache!") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) + journal.CurrentBuildEvent().PrepareComplete() err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) @@ -139,6 +145,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec common.Progress(11, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) + journal.CurrentBuildEvent().RecordComplete() } return nil diff --git a/htfs/library.go b/htfs/library.go index 8f8a5112..3556b7ea 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -34,6 +34,11 @@ type stats struct { dirty uint64 } +func (it *stats) Dirtyness() float64 { + dirtyness := (1000 * it.dirty) / it.total + return float64(dirtyness) / 10.0 +} + func (it *stats) Dirty(dirty bool) { it.Lock() defer it.Unlock() @@ -378,6 +383,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er common.TimelineEnd() defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + journal.CurrentBuildEvent().Dirty(score.Dirtyness()) fs.Controller = string(client) fs.Space = string(tag) err = fs.SaveAs(metafile) diff --git a/htfs/virtual.go b/htfs/virtual.go index 8ef7e4de..7c9e51d5 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -121,6 +121,7 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { common.Timeline("holotree restore done (virtual)") defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + journal.CurrentBuildEvent().Dirty(score.Dirtyness()) fs.Controller = string(client) fs.Space = string(tag) err = fs.SaveAs(metafile) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 1897fd56..9b4830e7 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -139,6 +139,7 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err common.TimelineEnd() defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) + journal.CurrentBuildEvent().Dirty(score.Dirtyness()) fs.Controller = string(client) fs.Space = string(tag) err = fs.SaveAs(metafile) diff --git a/journal/buildstats.go b/journal/buildstats.go new file mode 100644 index 00000000..fe68c6a2 --- /dev/null +++ b/journal/buildstats.go @@ -0,0 +1,155 @@ +package journal + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pretty" +) + +type ( + BuildEvent struct { + When int64 `json:"when"` + What string `json:"what"` + Force bool `json:"force"` + Build bool `json:"build"` + Success bool `json:"success"` + Retry bool `json:"retry"` + Run bool `json:"run"` + Controller string `json:"controller"` + Space string `json:"space"` + BlueprintHash string `json:"blueprint"` + + Started float64 `json:"started"` + Prepared float64 `json:"prepared"` + MicromambaDone float64 `json:"micromamba"` + PipDone float64 `json:"pip"` + PostInstallDone float64 `json:"postinstall"` + RecordDone float64 `json:"record"` + RestoreDone float64 `json:"restore"` + PreRunDone float64 `json:"prerun"` + RobotStart float64 `json:"robotstart"` + RobotEnd float64 `json:"robotend"` + Finished float64 `json:"finished"` + Dirtyness float64 `json:"dirtyness"` + } +) + +var ( + buildevent *BuildEvent +) + +func init() { + buildevent = NewBuildEvent() +} + +func CurrentBuildEvent() *BuildEvent { + return buildevent +} + +func CurrentEventFilename() string { + year, week := common.Clock.Time().ISOWeek() + filename := fmt.Sprintf("stats_%s_%04d_%02d.log", common.UserHomeIdentity(), year, week) + return filepath.Join(common.JournalLocation(), filename) +} + +func BuildEventStats(label string) { + err := serialize(buildevent.finished(label)) + if err != nil { + pretty.Warning("build stats for %q failed, reason: %v", label, err) + } +} + +func serialize(event *BuildEvent) (err error) { + defer fail.Around(&err) + + blob, err := json.Marshal(event) + fail.On(err != nil, "Could not serialize event: %v -> %v", event.What, err) + return appendJournal(CurrentEventFilename(), blob) +} + +func NewBuildEvent() *BuildEvent { + return &BuildEvent{ + When: common.Clock.When(), + } +} + +func (it *BuildEvent) stowatch() float64 { + return common.Clock.Elapsed().Seconds() +} + +func (it *BuildEvent) finished(label string) *BuildEvent { + it.What = label + it.Finished = it.stowatch() + it.Controller = common.ControllerType + it.Space = common.HolotreeSpace + return it +} + +func (it *BuildEvent) Successful() { + it.Success = true +} + +func (it *BuildEvent) StartNow(force bool) { + buildevent.Started = it.stowatch() + buildevent.Force = force +} + +func (it *BuildEvent) Blueprint(blueprint string) { + buildevent.BlueprintHash = blueprint +} + +func (it *BuildEvent) Rebuild() { + buildevent.Retry = true + buildevent.Build = true +} + +func (it *BuildEvent) PrepareComplete() { + buildevent.Build = true + buildevent.Prepared = it.stowatch() +} + +func (it *BuildEvent) MicromambaComplete() { + buildevent.Build = true + buildevent.MicromambaDone = it.stowatch() +} + +func (it *BuildEvent) PipComplete() { + buildevent.Build = true + buildevent.PipDone = it.stowatch() +} + +func (it *BuildEvent) PostInstallComplete() { + buildevent.Build = true + buildevent.PostInstallDone = it.stowatch() +} + +func (it *BuildEvent) RecordComplete() { + buildevent.RecordDone = it.stowatch() +} + +func (it *BuildEvent) Dirty(dirtyness float64) { + buildevent.Dirtyness = dirtyness +} + +func (it *BuildEvent) RestoreComplete() { + buildevent.RestoreDone = it.stowatch() +} + +func (it *BuildEvent) PreRunComplete() { + buildevent.Run = true + buildevent.PreRunDone = it.stowatch() +} + +func (it *BuildEvent) RobotStarts() { + buildevent.Run = true + buildevent.RobotStart = it.stowatch() +} + +func (it *BuildEvent) RobotEnds() { + buildevent.Run = true + buildevent.RobotEnd = it.stowatch() +} diff --git a/journal/journal.go b/journal/journal.go index c4c01bc0..89e264ca 100644 --- a/journal/journal.go +++ b/journal/journal.go @@ -40,18 +40,18 @@ func Post(event, detail, commentForm string, fields ...interface{}) (err error) } blob, err := json.Marshal(message) fail.On(err != nil, "Could not serialize event: %v -> %v", event, err) - return appendJournal(blob) + return appendJournal(common.EventJournal(), blob) } -func appendJournal(blob []byte) (err error) { +func appendJournal(journalname string, blob []byte) (err error) { defer fail.Around(&err) - handle, err := os.OpenFile(common.EventJournal(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) - fail.On(err != nil, "Failed to open event journal %v -> %v", common.EventJournal(), err) + handle, err := os.OpenFile(journalname, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) + fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) defer handle.Close() _, err = handle.Write(blob) - fail.On(err != nil, "Failed to write event journal %v -> %v", common.EventJournal(), err) + fail.On(err != nil, "Failed to write event journal %v -> %v", journalname, err) _, err = handle.Write([]byte{'\n'}) - fail.On(err != nil, "Failed to write event journal %v -> %v", common.EventJournal(), err) + fail.On(err != nil, "Failed to write event journal %v -> %v", journalname, err) return handle.Sync() } diff --git a/operations/running.go b/operations/running.go index 0f5014a4..0600a331 100644 --- a/operations/running.go +++ b/operations/running.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -252,6 +253,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro preRunScripts := config.PreRunScripts() if !common.DeveloperFlag && preRunScripts != nil && len(preRunScripts) > 0 { + common.Timeline("pre run scripts started") common.Debug("=== pre run script phase ===") for _, script := range preRunScripts { if !robot.PlatformAcceptableFile(runtime.GOARCH, runtime.GOOS, script) { @@ -268,14 +270,18 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro pretty.Exit(12, "%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) } } + journal.CurrentBuildEvent().PreRunComplete() + common.Timeline("pre run scripts completed") } common.Debug("about to run command - %v", task) + journal.CurrentBuildEvent().RobotStarts() if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) } else { _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } + journal.CurrentBuildEvent().RobotEnds() after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after, true) From 5b62402e9c1475add4663eb55cf5f4593162a6d6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 11 Nov 2022 10:30:25 +0200 Subject: [PATCH 318/516] FEATURE: command for holotree build statistics (v11.32.1) - feature: command to show local holotree environment build statistics --- cmd/holotreeStats.go | 40 ++++++++ common/version.go | 2 +- docs/changelog.md | 4 + journal/buildstats.go | 229 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 cmd/holotreeStats.go diff --git a/cmd/holotreeStats.go b/cmd/holotreeStats.go new file mode 100644 index 00000000..2e71a39a --- /dev/null +++ b/cmd/holotreeStats.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/journal" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + onlyAssistantStats bool + onlyRobotStats bool + onlyPrepareStats bool + onlyVariablesStats bool + statsWeeks uint +) + +var holotreeStatsCmd = &cobra.Command{ + Use: "statistics", + Short: "Show holotree environment build and runtime statistics.", + Long: "Show holotree environment build and runtime statistics.", + Aliases: []string{"statistic", "stats", "stat", "st"}, + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree stats calculation lasted").Report() + } + journal.ShowStatistics() + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeStatsCmd) + //holotreeStatsCmd.Flags().BoolVarP(&onlyAssistantStats, "--assistants", "a", false, "Include 'assistant run' into stats.") + //holotreeStatsCmd.Flags().BoolVarP(&onlyRobotStats, "--robots", "r", false, "Include 'robot run' into stats.") + //holotreeStatsCmd.Flags().BoolVarP(&onlyPrepareStats, "--prepares", "p", false, "Include 'cloud prepare' into stats.") + //holotreeStatsCmd.Flags().BoolVarP(&onlyVariablesStats, "--variables", "v", false, "Include 'holotree variables' into stats.") + //holotreeStatsCmd.Flags().UintVarP(&statsWeeks, "--weeks", "w", 12, "Number of previous weeks to include into stats.") +} diff --git a/common/version.go b/common/version.go index 811908c9..c008101f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.0` + Version = `v11.32.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index e0e6d11f..cdcbf4b5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.32.1 (date: 11.11.2022) UNSTABLE + +- feature: command to show local holotree environment build statistics + ## v11.32.0 (date: 10.11.2022) UNSTABLE - feature: local recording of holotree environment build statistics events diff --git a/journal/buildstats.go b/journal/buildstats.go index fe68c6a2..cf649a5c 100644 --- a/journal/buildstats.go +++ b/journal/buildstats.go @@ -1,9 +1,14 @@ package journal import ( + "bufio" "encoding/json" "fmt" + "io" + "os" "path/filepath" + "sort" + "text/tabwriter" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" @@ -11,7 +16,13 @@ import ( ) type ( - BuildEvent struct { + acceptor func(BuildEvent) bool + picker func(BuildEvent) float64 + prettify func(float64) string + + Numbers []float64 + BuildEvents []BuildEvent + BuildEvent struct { When int64 `json:"when"` What string `json:"what"` Force bool `json:"force"` @@ -46,6 +57,129 @@ func init() { buildevent = NewBuildEvent() } +func asPercent(value float64) string { + return fmt.Sprintf("%5.1f%%", value) +} + +func asSecond(value float64) string { + return fmt.Sprintf("%7.3fs", value) +} + +func started(the BuildEvent) float64 { + return the.Started +} + +func prepared(the BuildEvent) float64 { + return the.Prepared +} + +func micromamba(the BuildEvent) float64 { + return the.MicromambaDone +} + +func pip(the BuildEvent) float64 { + return the.PipDone +} + +func postinstall(the BuildEvent) float64 { + return the.PostInstallDone +} + +func record(the BuildEvent) float64 { + return the.RecordDone +} + +func restore(the BuildEvent) float64 { + return the.RestoreDone +} + +func prerun(the BuildEvent) float64 { + return the.PreRunDone +} + +func ShowStatistics() { + stats, err := Stats() + if err != nil { + pretty.Warning("Loading statistics failed, reason: %v", err) + return + } + tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', tabwriter.AlignRight) + tabbed.Write(sprint("Selected statistics:\t%d samples\t\n", len(stats))) + tabbed.Write([]byte("\n")) + tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) + stats.Statsline(tabbed, "Dirty", asPercent, func(the BuildEvent) float64 { + return the.Dirtyness + }) + tabbed.Write([]byte("\n")) + tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) + stats.Statsline(tabbed, "Lead time ", asSecond, func(the BuildEvent) float64 { + return the.Started + }) + stats.Statsline(tabbed, "Setup time ", asSecond, func(the BuildEvent) float64 { + if the.RobotStart > 0 { + return the.RobotStart - the.Started + } + return the.Finished - the.Started + }) + stats.Statsline(tabbed, "Holospace restore time", asSecond, func(the BuildEvent) float64 { + if the.RestoreDone > 0 { + return the.RestoreDone - the.Started + } + return the.Finished - the.Started + }) + stats.Statsline(tabbed, "Pre-run ", asSecond, func(the BuildEvent) float64 { + if the.PreRunDone > 0 { + return the.PreRunDone - the.RestoreDone + } + return 0 + }) + stats.Statsline(tabbed, "Robot startup delay ", asSecond, func(the BuildEvent) float64 { + return the.RobotStart + }) + stats.Statsline(tabbed, "Robot execution time ", asSecond, func(the BuildEvent) float64 { + return the.RobotEnd - the.RobotStart + }) + stats.Statsline(tabbed, "Total execution time ", asSecond, func(the BuildEvent) float64 { + return the.Finished + }) + onlyBuilds := stats.filter(func(the BuildEvent) bool { + return the.Build + }) + tabbed.Write([]byte("\n\n")) + percentage := 100.0 * float64(len(onlyBuilds)) / float64(len(stats)) + tabbed.Write(sprint("%d\tsamples with environment builds\t(%3.1f%% from selected)\t\n", len(onlyBuilds), percentage)) + tabbed.Write([]byte("\n")) + tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) + onlyBuilds.Statsline(tabbed, "Phase: prepare ", asSecond, func(the BuildEvent) float64 { + return the.Prepared - the.Started + }) + onlyBuilds.Statsline(tabbed, "Phase: micromamba ", asSecond, func(the BuildEvent) float64 { + return the.MicromambaDone - the.Prepared + }) + onlyBuilds.Statsline(tabbed, "Phase: pip ", asSecond, func(the BuildEvent) float64 { + if the.PipDone > 0 { + return the.PipDone - the.MicromambaDone + } + return 0.0 + }) + onlyBuilds.Statsline(tabbed, "Phase: post install ", asSecond, func(the BuildEvent) float64 { + if the.PostInstallDone > 0 { + return the.PostInstallDone - the.first(pip, micromamba) + } + return 0.0 + }) + onlyBuilds.Statsline(tabbed, "Phase: record ", asSecond, func(the BuildEvent) float64 { + return the.RecordDone - the.first(postinstall, pip, micromamba) + }) + onlyBuilds.Statsline(tabbed, "To hololib ", asSecond, func(the BuildEvent) float64 { + if the.RecordDone > 0 { + return the.RecordDone - the.Started + } + return 0.0 + }) + tabbed.Flush() +} + func CurrentBuildEvent() *BuildEvent { return buildevent } @@ -153,3 +287,96 @@ func (it *BuildEvent) RobotEnds() { buildevent.Run = true buildevent.RobotEnd = it.stowatch() } + +func (it BuildEvent) first(tools ...picker) float64 { + for _, tool := range tools { + value := tool(it) + if value > 0 { + return value + } + } + return 0 +} + +func (it BuildEvents) filter(query acceptor) BuildEvents { + result := make(BuildEvents, 0, len(it)) + for _, event := range it { + if query(event) { + result = append(result, event) + } + } + return result +} + +func (it BuildEvents) pick(tool picker) Numbers { + result := make(Numbers, 0, len(it)) + for _, event := range it { + result = append(result, tool(event)) + } + return result +} + +func sprint(form string, fields ...any) []byte { + return []byte(fmt.Sprintf(form, fields...)) +} + +func (it BuildEvents) Statsline(tabbed *tabwriter.Writer, label string, nice prettify, tool picker) { + numbers := it.pick(tool) + sort.Float64s(numbers) + average, low, median, high, last := numbers.Statsline() + tabbed.Write(sprint("%s\t%s\t%s\t%s\t%s\t%s\t\n", label, nice(average), nice(low), nice(median), nice(high), nice(last))) +} + +func (it Numbers) safe(at int) float64 { + total := len(it) + if total == 0 { + return 0.0 + } + if at < 0 { + return it[0] + } + if at < total { + return it[at] + } + return it[total-1] +} + +func (it Numbers) Statsline() (average, low, median, high, worst float64) { + total := len(it) + if total < 1 { + return + } + sum := 0.0 + for _, value := range it { + sum += value + } + half := total >> 1 + percentile := total / 10 + last := total - 1 + right := last - percentile + return sum / float64(total), it.safe(percentile), it.safe(half), it.safe(right), it.safe(last) +} + +func Stats() (result BuildEvents, err error) { + defer fail.Around(&err) + journalname := CurrentEventFilename() + handle, err := os.Open(journalname) + fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) + defer handle.Close() + source := bufio.NewReader(handle) + fail.On(err != nil, "Failed to read %s.", journalname) + result = make(BuildEvents, 0, 100) + for { + line, err := source.ReadBytes('\n') + if err == io.EOF { + return result, nil + } + fail.On(err != nil, "Failed to read %s.", journalname) + event := BuildEvent{} + err = json.Unmarshal(line, &event) + if err != nil { + continue + } + result = append(result, event) + } +} From a726e71d4934ed34422864f84320d9f8d92e930a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 11 Nov 2022 12:55:50 +0200 Subject: [PATCH 319/516] IMPROVEMENT: holotree build statistics flags (v11.32.2) - added week limitation option for holotree statistics command - added filter flags for assistants, robots, prepares, and variables for holotree statistics command --- cmd/holotreeStats.go | 12 +++--- common/version.go | 2 +- docs/changelog.md | 6 +++ journal/buildstats.go | 94 +++++++++++++++++++++++++++++++++---------- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/cmd/holotreeStats.go b/cmd/holotreeStats.go index 2e71a39a..90fe8983 100644 --- a/cmd/holotreeStats.go +++ b/cmd/holotreeStats.go @@ -25,16 +25,16 @@ var holotreeStatsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree stats calculation lasted").Report() } - journal.ShowStatistics() + journal.ShowStatistics(statsWeeks, onlyAssistantStats, onlyRobotStats, onlyPrepareStats, onlyVariablesStats) pretty.Ok() }, } func init() { holotreeCmd.AddCommand(holotreeStatsCmd) - //holotreeStatsCmd.Flags().BoolVarP(&onlyAssistantStats, "--assistants", "a", false, "Include 'assistant run' into stats.") - //holotreeStatsCmd.Flags().BoolVarP(&onlyRobotStats, "--robots", "r", false, "Include 'robot run' into stats.") - //holotreeStatsCmd.Flags().BoolVarP(&onlyPrepareStats, "--prepares", "p", false, "Include 'cloud prepare' into stats.") - //holotreeStatsCmd.Flags().BoolVarP(&onlyVariablesStats, "--variables", "v", false, "Include 'holotree variables' into stats.") - //holotreeStatsCmd.Flags().UintVarP(&statsWeeks, "--weeks", "w", 12, "Number of previous weeks to include into stats.") + holotreeStatsCmd.Flags().BoolVarP(&onlyAssistantStats, "--assistants", "a", false, "Include 'assistant run' into stats.") + holotreeStatsCmd.Flags().BoolVarP(&onlyRobotStats, "--robots", "r", false, "Include 'robot run' into stats.") + holotreeStatsCmd.Flags().BoolVarP(&onlyPrepareStats, "--prepares", "p", false, "Include 'cloud prepare' into stats.") + holotreeStatsCmd.Flags().BoolVarP(&onlyVariablesStats, "--variables", "v", false, "Include 'holotree variables' into stats.") + holotreeStatsCmd.Flags().UintVarP(&statsWeeks, "--weeks", "w", 12, "Number of previous weeks to include into stats.") } diff --git a/common/version.go b/common/version.go index c008101f..92852121 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.1` + Version = `v11.32.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index cdcbf4b5..3013d11f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.32.2 (date: 11.11.2022) + +- added week limitation option for holotree statistics command +- added filter flags for assistants, robots, prepares, and variables for + holotree statistics command + ## v11.32.1 (date: 11.11.2022) UNSTABLE - feature: command to show local holotree environment build statistics diff --git a/journal/buildstats.go b/journal/buildstats.go index cf649a5c..cc2dff9a 100644 --- a/journal/buildstats.go +++ b/journal/buildstats.go @@ -8,10 +8,13 @@ import ( "os" "path/filepath" "sort" + "strings" "text/tabwriter" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" ) @@ -97,14 +100,41 @@ func prerun(the BuildEvent) float64 { return the.PreRunDone } -func ShowStatistics() { - stats, err := Stats() +func anyOf(flags ...bool) bool { + for _, flag := range flags { + if flag { + return true + } + } + return false +} + +func ShowStatistics(weeks uint, assistants, robots, prepares, variables bool) { + stats, err := Stats(weeks) + selected := []string{"all"} if err != nil { pretty.Warning("Loading statistics failed, reason: %v", err) return } + if anyOf(assistants, robots, prepares, variables) { + selectors := make(map[string]bool) + selectors["assistant"] = assistants + selectors["robot"] = robots + selectors["prepare"] = prepares + selectors["variables"] = variables + stats = stats.filter(func(the BuildEvent) bool { + return selectors[the.What] + }) + selected = []string{} + for key, value := range selectors { + if value { + selected = append(selected, key) + } + } + sort.Strings(selected) + } tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', tabwriter.AlignRight) - tabbed.Write(sprint("Selected statistics:\t%d samples\t\n", len(stats))) + tabbed.Write(sprint("Selected (%s) statistics: %d samples [%d full weeks]\t\n", strings.Join(selected, ", "), len(stats), weeks)) tabbed.Write([]byte("\n")) tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) stats.Statsline(tabbed, "Dirty", asPercent, func(the BuildEvent) float64 { @@ -184,12 +214,28 @@ func CurrentBuildEvent() *BuildEvent { return buildevent } -func CurrentEventFilename() string { - year, week := common.Clock.Time().ISOWeek() +func BuildEventFilenameFor(stamp time.Time) string { + year, week := stamp.ISOWeek() filename := fmt.Sprintf("stats_%s_%04d_%02d.log", common.UserHomeIdentity(), year, week) return filepath.Join(common.JournalLocation(), filename) } +func CurrentEventFilename() string { + return BuildEventFilenameFor(common.Clock.Time()) +} + +func BuildEventFilenamesFor(weekcount int) []string { + weekstep := -7 * 24 * time.Hour + timestamp := common.Clock.Time() + result := make([]string, 0, weekcount+1) + for weekcount >= 0 { + result = append(result, BuildEventFilenameFor(timestamp)) + timestamp = timestamp.Add(weekstep) + weekcount-- + } + return result +} + func BuildEventStats(label string) { err := serialize(buildevent.finished(label)) if err != nil { @@ -357,26 +403,32 @@ func (it Numbers) Statsline() (average, low, median, high, worst float64) { return sum / float64(total), it.safe(percentile), it.safe(half), it.safe(right), it.safe(last) } -func Stats() (result BuildEvents, err error) { +func Stats(weeks uint) (result BuildEvents, err error) { defer fail.Around(&err) - journalname := CurrentEventFilename() - handle, err := os.Open(journalname) - fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) - defer handle.Close() - source := bufio.NewReader(handle) - fail.On(err != nil, "Failed to read %s.", journalname) result = make(BuildEvents, 0, 100) - for { - line, err := source.ReadBytes('\n') - if err == io.EOF { - return result, nil + for _, journalname := range BuildEventFilenamesFor(int(weeks)) { + if !pathlib.IsFile(journalname) { + continue } + handle, err := os.Open(journalname) + fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) + defer handle.Close() + source := bufio.NewReader(handle) fail.On(err != nil, "Failed to read %s.", journalname) - event := BuildEvent{} - err = json.Unmarshal(line, &event) - if err != nil { - continue + innerloop: + for { + line, err := source.ReadBytes('\n') + if err == io.EOF { + break innerloop + } + fail.On(err != nil, "Failed to read %s.", journalname) + event := BuildEvent{} + err = json.Unmarshal(line, &event) + if err != nil { + continue innerloop + } + result = append(result, event) } - result = append(result, event) } + return result, nil } From f18cf82e5f31d931b03aeed1559742335a06957a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 14 Nov 2022 16:27:04 +0200 Subject: [PATCH 320/516] IMPROVEMENT: statistics in diagnostics (v11.32.3) - holotree statistics are now part of human readable diagnostics when there is 5 or more entries in statistics (but not available in JSON output) - added cumulative statistics section into output - bugfix: calculation mistakes in case of missing steps - bugfix: detecting successful build --- common/version.go | 2 +- conda/workflows.go | 4 +- docs/changelog.md | 8 ++ journal/buildstats.go | 236 +++++++++++++++++++++++++++++++------- operations/diagnostics.go | 8 ++ 5 files changed, 215 insertions(+), 43 deletions(-) diff --git a/common/version.go b/common/version.go index 92852121..7392065f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.2` + Version = `v11.32.3` ) diff --git a/conda/workflows.go b/conda/workflows.go index 76c3f570..8231932c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -116,7 +116,9 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall } success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, postInstall) } - journal.CurrentBuildEvent().Successful() + if success { + journal.CurrentBuildEvent().Successful() + } return success, nil } diff --git a/docs/changelog.md b/docs/changelog.md index 3013d11f..86f2f53f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.32.3 (date: 14.11.2022) + +- holotree statistics are now part of human readable diagnostics when there + is 5 or more entries in statistics (but not available in JSON output) +- added cumulative statistics section into output +- bugfix: calculation mistakes in case of missing steps +- bugfix: detecting successful build + ## v11.32.2 (date: 11.11.2022) - added week limitation option for holotree statistics command diff --git a/journal/buildstats.go b/journal/buildstats.go index cc2dff9a..9b2e9283 100644 --- a/journal/buildstats.go +++ b/journal/buildstats.go @@ -2,6 +2,7 @@ package journal import ( "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -19,13 +20,15 @@ import ( ) type ( - acceptor func(BuildEvent) bool - picker func(BuildEvent) float64 + acceptor func(*BuildEvent) bool + picker func(*BuildEvent) float64 + flagger func(*BuildEvent) bool prettify func(float64) string Numbers []float64 - BuildEvents []BuildEvent + BuildEvents []*BuildEvent BuildEvent struct { + Version string `json:"version"` When int64 `json:"when"` What string `json:"what"` Force bool `json:"force"` @@ -52,6 +55,13 @@ type ( } ) +const ( + assistantKey = "assistant" + prepareKey = "prepare" + robotKey = "robot" + variableKey = "variables" +) + var ( buildevent *BuildEvent ) @@ -68,38 +78,70 @@ func asSecond(value float64) string { return fmt.Sprintf("%7.3fs", value) } -func started(the BuildEvent) float64 { +func asCount(value int) string { + return fmt.Sprintf("%d", value) +} + +func forced(the *BuildEvent) bool { + return the.Force +} + +func retried(the *BuildEvent) bool { + return the.Retry +} + +func failed(the *BuildEvent) bool { + return the.Build && !the.Success +} + +func build(the *BuildEvent) bool { + return the.Build +} + +func started(the *BuildEvent) float64 { return the.Started } -func prepared(the BuildEvent) float64 { +func prepared(the *BuildEvent) float64 { return the.Prepared } -func micromamba(the BuildEvent) float64 { +func micromamba(the *BuildEvent) float64 { return the.MicromambaDone } -func pip(the BuildEvent) float64 { +func pip(the *BuildEvent) float64 { return the.PipDone } -func postinstall(the BuildEvent) float64 { +func postinstall(the *BuildEvent) float64 { return the.PostInstallDone } -func record(the BuildEvent) float64 { +func record(the *BuildEvent) float64 { return the.RecordDone } -func restore(the BuildEvent) float64 { +func restore(the *BuildEvent) float64 { return the.RestoreDone } -func prerun(the BuildEvent) float64 { +func prerun(the *BuildEvent) float64 { return the.PreRunDone } +func robotstarts(the *BuildEvent) float64 { + return the.RobotStart +} + +func robotends(the *BuildEvent) float64 { + return the.RobotEnd +} + +func finished(the *BuildEvent) float64 { + return the.Finished +} + func anyOf(flags ...bool) bool { for _, flag := range flags { if flag { @@ -110,19 +152,26 @@ func anyOf(flags ...bool) bool { } func ShowStatistics(weeks uint, assistants, robots, prepares, variables bool) { + _, body := MakeStatistics(weeks, assistants, robots, prepares, variables) + os.Stderr.Write(body) + os.Stderr.Sync() +} + +func MakeStatistics(weeks uint, assistants, robots, prepares, variables bool) (int, []byte) { + sink := bytes.NewBuffer(nil) stats, err := Stats(weeks) selected := []string{"all"} if err != nil { pretty.Warning("Loading statistics failed, reason: %v", err) - return + return 0, sink.Bytes() } if anyOf(assistants, robots, prepares, variables) { selectors := make(map[string]bool) - selectors["assistant"] = assistants - selectors["robot"] = robots - selectors["prepare"] = prepares - selectors["variables"] = variables - stats = stats.filter(func(the BuildEvent) bool { + selectors[assistantKey] = assistants + selectors[robotKey] = robots + selectors[prepareKey] = prepares + selectors[variableKey] = variables + stats = stats.filter(func(the *BuildEvent) bool { return selectors[the.What] }) selected = []string{} @@ -133,81 +182,185 @@ func ShowStatistics(weeks uint, assistants, robots, prepares, variables bool) { } sort.Strings(selected) } - tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', tabwriter.AlignRight) + tabbed := tabwriter.NewWriter(sink, 2, 4, 2, ' ', tabwriter.AlignRight) tabbed.Write(sprint("Selected (%s) statistics: %d samples [%d full weeks]\t\n", strings.Join(selected, ", "), len(stats), weeks)) tabbed.Write([]byte("\n")) tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) - stats.Statsline(tabbed, "Dirty", asPercent, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Dirty", asPercent, func(the *BuildEvent) float64 { return the.Dirtyness }) tabbed.Write([]byte("\n")) tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) - stats.Statsline(tabbed, "Lead time ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Lead time ", asSecond, func(the *BuildEvent) float64 { return the.Started }) - stats.Statsline(tabbed, "Setup time ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Setup time ", asSecond, func(the *BuildEvent) float64 { if the.RobotStart > 0 { return the.RobotStart - the.Started } return the.Finished - the.Started }) - stats.Statsline(tabbed, "Holospace restore time", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Holospace restore time", asSecond, func(the *BuildEvent) float64 { if the.RestoreDone > 0 { return the.RestoreDone - the.Started } return the.Finished - the.Started }) - stats.Statsline(tabbed, "Pre-run ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Pre-run ", asSecond, func(the *BuildEvent) float64 { if the.PreRunDone > 0 { return the.PreRunDone - the.RestoreDone } return 0 }) - stats.Statsline(tabbed, "Robot startup delay ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Robot startup delay ", asSecond, func(the *BuildEvent) float64 { return the.RobotStart }) - stats.Statsline(tabbed, "Robot execution time ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Robot execution time ", asSecond, func(the *BuildEvent) float64 { return the.RobotEnd - the.RobotStart }) - stats.Statsline(tabbed, "Total execution time ", asSecond, func(the BuildEvent) float64 { + stats.Statsline(tabbed, "Total execution time ", asSecond, func(the *BuildEvent) float64 { return the.Finished }) - onlyBuilds := stats.filter(func(the BuildEvent) bool { + onlyBuilds := stats.filter(func(the *BuildEvent) bool { return the.Build }) tabbed.Write([]byte("\n\n")) - percentage := 100.0 * float64(len(onlyBuilds)) / float64(len(stats)) + statCount := len(stats) + percentage := 100.0 * float64(len(onlyBuilds)) / float64(statCount) tabbed.Write(sprint("%d\tsamples with environment builds\t(%3.1f%% from selected)\t\n", len(onlyBuilds), percentage)) tabbed.Write([]byte("\n")) tabbed.Write([]byte("Name \tAverage\t10%\tMedian\t90%\tMAX\t\n")) - onlyBuilds.Statsline(tabbed, "Phase: prepare ", asSecond, func(the BuildEvent) float64 { - return the.Prepared - the.Started + onlyBuilds.Statsline(tabbed, "Phase: prepare ", asSecond, func(the *BuildEvent) float64 { + if the.Prepared > 0 { + return the.Prepared - the.Started + } + return 0.0 }) - onlyBuilds.Statsline(tabbed, "Phase: micromamba ", asSecond, func(the BuildEvent) float64 { - return the.MicromambaDone - the.Prepared + onlyBuilds.Statsline(tabbed, "Phase: micromamba ", asSecond, func(the *BuildEvent) float64 { + if the.MicromambaDone > 0 { + return the.MicromambaDone - the.Prepared + } + return 0.0 }) - onlyBuilds.Statsline(tabbed, "Phase: pip ", asSecond, func(the BuildEvent) float64 { + onlyBuilds.Statsline(tabbed, "Phase: pip ", asSecond, func(the *BuildEvent) float64 { if the.PipDone > 0 { return the.PipDone - the.MicromambaDone } return 0.0 }) - onlyBuilds.Statsline(tabbed, "Phase: post install ", asSecond, func(the BuildEvent) float64 { + onlyBuilds.Statsline(tabbed, "Phase: post install ", asSecond, func(the *BuildEvent) float64 { if the.PostInstallDone > 0 { return the.PostInstallDone - the.first(pip, micromamba) } return 0.0 }) - onlyBuilds.Statsline(tabbed, "Phase: record ", asSecond, func(the BuildEvent) float64 { - return the.RecordDone - the.first(postinstall, pip, micromamba) + onlyBuilds.Statsline(tabbed, "Phase: record ", asSecond, func(the *BuildEvent) float64 { + if the.RecordDone > 0 { + return the.RecordDone - the.first(postinstall, pip, micromamba) + } + return 0.0 }) - onlyBuilds.Statsline(tabbed, "To hololib ", asSecond, func(the BuildEvent) float64 { + onlyBuilds.Statsline(tabbed, "To hololib ", asSecond, func(the *BuildEvent) float64 { if the.RecordDone > 0 { return the.RecordDone - the.Started } return 0.0 }) + + assistantStats := selectStats(stats, assistantKey) + prepareStats := selectStats(stats, prepareKey) + robotStats := selectStats(stats, robotKey) + variableStats := selectStats(stats, variableKey) + + tabbed.Write([]byte("\n\n")) + tabbed.Write([]byte("Cumulative \tAssistants\tPrepares\tRobots\tVariables\tTotal\t\n")) + tabbed.Write(tabs("Run counts ", theSize(assistantStats), theSize(prepareStats), theSize(robotStats), theSize(variableStats), theSize(stats))) + tabbed.Write(tabs("Build counts ", + theCounts(assistantStats, build), + theCounts(prepareStats, build), + theCounts(robotStats, build), + theCounts(variableStats, build), + theCounts(stats, build))) + tabbed.Write(tabs("Forced builds ", + theCounts(assistantStats, forced), + theCounts(prepareStats, forced), + theCounts(robotStats, forced), + theCounts(variableStats, forced), + theCounts(stats, forced))) + tabbed.Write(tabs("Retried builds", + theCounts(assistantStats, retried), + theCounts(prepareStats, retried), + theCounts(robotStats, retried), + theCounts(variableStats, retried), + theCounts(stats, retried))) + tabbed.Write(tabs("Failed builds ", + theCounts(assistantStats, failed), + theCounts(prepareStats, failed), + theCounts(robotStats, failed), + theCounts(variableStats, failed), + theCounts(stats, failed))) + tabbed.Write(tabs("Setup times ", + theTimes(assistantStats, priority(robotstarts, finished), priority(started)), + theTimes(prepareStats, priority(robotstarts, finished), priority(started)), + theTimes(robotStats, priority(robotstarts, finished), priority(started)), + theTimes(variableStats, priority(robotstarts, finished), priority(started)), + theTimes(stats, priority(robotstarts, finished), priority(started)))) + tabbed.Write(tabs("Run times ", + theTimes(assistantStats, priority(robotends), priority(robotstarts)), + theTimes(prepareStats, priority(robotends), priority(robotstarts)), + theTimes(robotStats, priority(robotends), priority(robotstarts)), + theTimes(variableStats, priority(robotends), priority(robotstarts)), + theTimes(stats, priority(robotends), priority(robotstarts)))) + tabbed.Write(tabs("Total times ", + theTimes(assistantStats, priority(finished), priority(started)), + theTimes(prepareStats, priority(finished), priority(started)), + theTimes(robotStats, priority(finished), priority(started)), + theTimes(variableStats, priority(finished), priority(started)), + theTimes(stats, priority(finished), priority(started)))) tabbed.Flush() + return statCount, sink.Bytes() +} + +func theCounts(source BuildEvents, check flagger) string { + total := 0 + for _, event := range source { + if check(event) { + total++ + } + } + return asCount(total) +} + +func priority(pickers ...picker) []picker { + return pickers +} + +func theTimes(source BuildEvents, till []picker, from []picker) string { + total := 0.0 + for _, event := range source { + done := event.first(till...) + if done > 0.0 { + area := done - event.first(from...) + total += area + } + } + return asSecond(total) +} + +func theSize(source BuildEvents) string { + return fmt.Sprintf("%d", len(source)) +} + +func selectStats(source BuildEvents, key string) BuildEvents { + result := source.filter(func(the *BuildEvent) bool { + return the.What == key + }) + return result +} + +func tabs(columns ...any) []byte { + form := strings.Repeat("%s\t", len(columns)) + "\n" + return []byte(fmt.Sprintf(form, columns...)) } func CurrentBuildEvent() *BuildEvent { @@ -253,7 +406,8 @@ func serialize(event *BuildEvent) (err error) { func NewBuildEvent() *BuildEvent { return &BuildEvent{ - When: common.Clock.When(), + When: common.Clock.When(), + Version: common.Version, } } @@ -334,7 +488,7 @@ func (it *BuildEvent) RobotEnds() { buildevent.RobotEnd = it.stowatch() } -func (it BuildEvent) first(tools ...picker) float64 { +func (it *BuildEvent) first(tools ...picker) float64 { for _, tool := range tools { value := tool(it) if value > 0 { @@ -422,8 +576,8 @@ func Stats(weeks uint) (result BuildEvents, err error) { break innerloop } fail.On(err != nil, "Failed to read %s.", journalname) - event := BuildEvent{} - err = json.Unmarshal(line, &event) + event := &BuildEvent{} + err = json.Unmarshal(line, event) if err != nil { continue innerloop } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 2cf0f79f..28718657 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -18,6 +18,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" @@ -420,6 +421,13 @@ func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { for _, check := range details.Checks { fmt.Fprintf(sink, " - %-8s %-8s %s\n", check.Type, check.Status, check.Message) } + count, body := journal.MakeStatistics(12, false, false, false, false) + if count > 4 { + fmt.Fprintln(sink, "") + fmt.Fprintln(sink, "Statistics:") + fmt.Fprintln(sink, "") + fmt.Fprintln(sink, string(body)) + } } func fileIt(filename string) (io.WriteCloser, error) { From d9bde44c6bdda4166f801f77823495e4601cd039 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 15 Nov 2022 09:54:21 +0200 Subject: [PATCH 321/516] CLEANUP: old statistics removal (v11.32.4) - cleanup: removing old run minutes and stat lines (holotree stats cover those) --- cmd/assistantRun.go | 2 -- cmd/carrier.go | 2 -- cmd/run.go | 2 -- cmd/testrun.go | 2 -- common/version.go | 2 +- conda/workflows.go | 18 ------------------ docs/changelog.md | 4 ++++ operations/diagnostics.go | 11 ----------- xviper/runminutes.go | 25 ------------------------- xviper/runminutes_test.go | 22 ---------------------- 10 files changed, 5 insertions(+), 85 deletions(-) delete mode 100644 xviper/runminutes.go delete mode 100644 xviper/runminutes_test.go diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index ce02d3a1..a44cdf83 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -14,7 +14,6 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" - "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" ) @@ -33,7 +32,6 @@ var assistantRunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Robot Assistant run lasted").Report() } - defer xviper.RunMinutes().Done() account := operations.AccountByName(AccountName()) if account == nil { pretty.Exit(1, "Could not find account by name: %q", AccountName()) diff --git a/cmd/carrier.go b/cmd/carrier.go index ac3bdbd3..5a0db575 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -12,7 +12,6 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" - "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" ) @@ -42,7 +41,6 @@ func runCarrier() error { if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } - defer xviper.RunMinutes().Done() now := time.Now() testrunDir := filepath.Join(".", now.Format("2006-01-02_15_04_05")) err = os.MkdirAll(testrunDir, 0o755) diff --git a/cmd/run.go b/cmd/run.go index 19856fcb..283ce787 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -6,7 +6,6 @@ import ( "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" ) @@ -29,7 +28,6 @@ in your own machine.`, if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() } - defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) commandline := todo.Commandline() diff --git a/cmd/testrun.go b/cmd/testrun.go index 3160b7b6..358d5aee 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -13,7 +13,6 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" - "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" ) @@ -28,7 +27,6 @@ var testrunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } - defer xviper.RunMinutes().Done() now := time.Now() zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("testrun%x.zip", common.When)) defer os.Remove(zipfile) diff --git a/common/version.go b/common/version.go index 7392065f..bdc9b600 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.3` + Version = `v11.32.4` ) diff --git a/conda/workflows.go b/conda/workflows.go index 8231932c..a60a2c36 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -18,7 +18,6 @@ import ( "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" - "github.com/robocorp/rcc/xviper" ) const ( @@ -349,26 +348,13 @@ func LegacyEnvironment(force bool, configurations ...string) error { } defer locker.Release() - requests := xviper.GetInt("stats.env.request") + 1 - misses := xviper.GetInt("stats.env.miss") - failures := xviper.GetInt("stats.env.failures") - merges := xviper.GetInt("stats.env.merges") freshInstall := true - xviper.Set("stats.env.request", requests) - - if len(configurations) > 1 { - merges += 1 - xviper.Set("stats.env.merges", merges) - } - condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", common.When)) requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", common.When)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) if err != nil { - failures += 1 - xviper.Set("stats.env.failures", failures) return err } defer os.Remove(condaYaml) @@ -379,13 +365,9 @@ func LegacyEnvironment(force bool, configurations ...string) error { return err } if success { - misses += 1 - xviper.Set("stats.env.miss", misses) return nil } - failures += 1 - xviper.Set("stats.env.failures", failures) return errors.New("Could not create environment.") } diff --git a/docs/changelog.md b/docs/changelog.md index 86f2f53f..14da8f0d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.32.4 (date: 15.11.2022) + +- cleanup: removing old run minutes and stat lines (holotree stats cover those) + ## v11.32.3 (date: 14.11.2022) - holotree statistics are now part of human readable diagnostics when there diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 28718657..870de358 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -66,7 +66,6 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["executable"] = common.BinRcc() result.Details["rcc"] = common.Version result.Details["rcc.bin"] = common.BinRcc() - result.Details["stats"] = rccStatusLine() result.Details["micromamba"] = conda.MicromambaVersion() result.Details["micromamba.bin"] = conda.BinMicromamba() result.Details["ROBOCORP_HOME"] = common.RobocorpHome() @@ -146,16 +145,6 @@ func lockfiles() map[string]string { return result } -func rccStatusLine() string { - requests := xviper.GetInt("stats.env.request") - hits := xviper.GetInt("stats.env.hit") - dirty := xviper.GetInt("stats.env.dirty") - misses := xviper.GetInt("stats.env.miss") - failures := xviper.GetInt("stats.env.failures") - merges := xviper.GetInt("stats.env.merges") - return fmt.Sprintf("%d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s", requests, merges, hits, dirty, misses, failures, xviper.TrackingIdentity()) -} - func longPathSupportCheck() *common.DiagnosticCheck { supportLongPathUrl := settings.Global.DocsLink("troubleshooting/windows-long-path") if conda.HasLongPathSupport() { diff --git a/xviper/runminutes.go b/xviper/runminutes.go deleted file mode 100644 index cd21dc91..00000000 --- a/xviper/runminutes.go +++ /dev/null @@ -1,25 +0,0 @@ -package xviper - -import ( - "math" - "time" -) - -const ( - runMinutesStats = `stats.rccminutes` -) - -type runMarker time.Time - -func RunMinutes() runMarker { - return runMarker(time.Now()) -} - -func (it runMarker) Done() uint64 { - delta := time.Now().Sub(time.Time(it)) - minutes := uint64(math.Max(1.0, math.Ceil(delta.Minutes()))) - previous := GetUint64(runMinutesStats) - total := previous + minutes - Set(runMinutesStats, total) - return total -} diff --git a/xviper/runminutes_test.go b/xviper/runminutes_test.go deleted file mode 100644 index 5554d4a2..00000000 --- a/xviper/runminutes_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package xviper_test - -import ( - "testing" - "time" - - "github.com/robocorp/rcc/hamlet" - "github.com/robocorp/rcc/xviper" -) - -func TestCanCreateRunMinutes(t *testing.T) { - must_be, wont_be := hamlet.Specifications(t) - - sut := xviper.RunMinutes() - wont_be.Nil(sut) - time.Sleep(100 * time.Millisecond) - first := sut.Done() - must_be.True(first > 0) - second := xviper.RunMinutes().Done() - must_be.True(second > first) - must_be.Equal(first+1, second) -} From 52c542d7ba95c446b02eed1397214b5adb0ea335 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 15 Nov 2022 10:35:31 +0200 Subject: [PATCH 322/516] MAINTENANCE: dead code removal (v11.32.5) - cleanup: removing dead code that was not used anymore --- common/version.go | 2 +- docs/changelog.md | 4 ++++ pathlib/functions.go | 4 ---- xviper/wrapper.go | 8 -------- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/common/version.go b/common/version.go index bdc9b600..6b067923 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.4` + Version = `v11.32.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 14da8f0d..499a7d73 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.32.5 (date: 15.11.2022) + +- cleanup: removing dead code that was not used anymore + ## v11.32.4 (date: 15.11.2022) - cleanup: removing old run minutes and stat lines (holotree stats cover those) diff --git a/pathlib/functions.go b/pathlib/functions.go index 8dd527b2..9f0745bb 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -170,10 +170,6 @@ func EnsureDirectory(directory string) (string, error) { return doEnsureDirectory(directory, 0o750) } -func EnsureParentDirectory(resource string) (string, error) { - return EnsureDirectory(filepath.Dir(resource)) -} - func RemoveEmptyDirectores(starting string) (err error) { defer fail.Around(&err) diff --git a/xviper/wrapper.go b/xviper/wrapper.go index c2f7c6ce..19373c5e 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -180,14 +180,6 @@ func GetInt64(key string) int64 { return <-flow } -func GetInt(key string) int { - flow := make(chan int) - pipeline <- func(core *config) { - flow <- core.Summon().GetInt(key) - } - return <-flow -} - func GetString(key string) string { flow := make(chan string) pipeline <- func(core *config) { From 33e453c5f8964179fd8d72773792114ff0d9f7e2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 15 Nov 2022 13:23:22 +0200 Subject: [PATCH 323/516] BUGFIX: lock pid files warning/ok clarification (v11.32.6) - bugfix: from now on, lock pid files will only give diagnostic "warning" when they are less than 12 hours old, after that they will be labeled as "stale" and will still be visible in diagnostics, but on "ok" level --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 6b067923..0a6619ae 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.5` + Version = `v11.32.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index 499a7d73..54ea5329 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.32.6 (date: 15.11.2022) + +- bugfix: from now on, lock pid files will only give diagnostic "warning" when + they are less than 12 hours old, after that they will be labeled as "stale" + and will still be visible in diagnostics, but on "ok" level + ## v11.32.5 (date: 15.11.2022) - cleanup: removing dead code that was not used anymore diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 870de358..9fd6da26 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -205,11 +205,17 @@ func lockpidsCheck() []*common.DiagnosticCheck { }) return result } + deadline := time.Now().Add(-12 * time.Hour) for _, entry := range entries { + level, qualifier := statusWarning, "Pending" + info, err := entry.Info() + if err == nil && info.ModTime().Before(deadline) { + level, qualifier = statusOk, "Stale(?)" + } result = append(result, &common.DiagnosticCheck{ Type: "OS", - Status: statusWarning, - Message: fmt.Sprintf("Pending lock file info: %q", entry.Name()), + Status: level, + Message: fmt.Sprintf("%s lock file info: %q", qualifier, entry.Name()), Link: support, }) } From 6de041e0195dbe49e6ec36f73d2943b5d12a00f2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 18 Nov 2022 11:43:26 +0200 Subject: [PATCH 324/516] FEATURE: holotree delta export (v11.33.0) - feature: holotree delta export (for missing things only) - changes normal holotree export command to support ".hld" files --- cmd/holotreeExport.go | 85 +++++++++++++++++++++++++++++++++++++++++-- common/version.go | 2 +- docs/changelog.md | 5 +++ htfs/functions.go | 23 ++++++++++++ htfs/library.go | 20 +++++++++- htfs/unmanaged.go | 2 +- htfs/virtual.go | 2 +- 7 files changed, 131 insertions(+), 8 deletions(-) diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index 86f3eb83..7b87a813 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -2,20 +2,79 @@ package cmd import ( "encoding/json" + "fmt" + "os" + "sort" "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" ) var ( holozip string exportRobot string + specFile string ) -func holotreeExport(catalogs []string, archive string) { +type ( + ExportSpec struct { + Workspace string `yaml:"workspace"` + Known []string `yaml:"knows"` + Needs []string `yaml:"wants"` + } +) + +func specFrom(content []byte) (*ExportSpec, error) { + result := &ExportSpec{} + err := yaml.Unmarshal(content, result) + if err != nil { + return nil, err + } + return result, nil +} + +func loadExportSpec(filename string) (*ExportSpec, error) { + raw, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + spec, err := specFrom(raw) + if err != nil { + return nil, err + } + return spec, nil +} + +func exportBySpecification(filename string) { + spec, err := loadExportSpec(filename) + pretty.Guard(err == nil, 4, "Loading specification %q failed, reason: %v", filename, err) + known := selectExactCatalogs(spec.Known) + needs := selectExactCatalogs(spec.Needs) + pretty.Guard(len(spec.Needs) == len(needs), 5, "Only %d out of %d needed catalogs available. Quitting!", len(needs), len(spec.Needs)) + unifiedSpec := &ExportSpec{ + Workspace: spec.Workspace, + Known: known, + Needs: needs, + } + content, err := yaml.Marshal(unifiedSpec) + pretty.Guard(err == nil, 6, "Marshaling unified specification failed, reason: %v", err) + fingerprint := common.Siphash(9007199254740993, 2147483647, content) + common.Debug("Final delta specification %0x16x is:\n%s", fingerprint, string(content)) + deltafile := fmt.Sprintf("%016x.hld", fingerprint) + holotreeExport(needs, known, deltafile) + common.Stdout("%s\n", deltafile) +} + +func holotreeExport(catalogs, known []string, archive string) { + common.Debug("Ignoring content from catalogs:") + for _, catalog := range known { + common.Debug("- %s", catalog) + } + common.Debug("Exporting catalogs:") for _, catalog := range catalogs { common.Debug("- %s", catalog) @@ -24,7 +83,7 @@ func holotreeExport(catalogs []string, archive string) { tree, err := htfs.New() pretty.Guard(err == nil, 2, "%s", err) - err = tree.Export(catalogs, archive) + err = tree.Export(catalogs, known, archive) pretty.Guard(err == nil, 3, "%s", err) } @@ -41,6 +100,20 @@ func listCatalogs(jsonForm bool) { } } +func selectExactCatalogs(filters []string) []string { + result := make([]string, 0, len(filters)) + for _, catalog := range htfs.Catalogs() { + for _, filter := range filters { + if catalog == filter { + result = append(result, catalog) + break + } + } + } + sort.Strings(result) + return result +} + func selectCatalogs(filters []string) []string { result := make([]string, 0, len(filters)) for _, catalog := range htfs.Catalogs() { @@ -51,6 +124,7 @@ func selectCatalogs(filters []string) []string { } } } + sort.Strings(result) return result } @@ -62,6 +136,10 @@ var holotreeExportCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree export command lasted").Report() } + if len(specFile) > 0 { + exportBySpecification(specFile) + return + } if len(exportRobot) > 0 { _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, exportRobot) pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) @@ -71,7 +149,7 @@ var holotreeExportCmd = &cobra.Command{ if len(args) == 0 { listCatalogs(jsonFlag) } else { - holotreeExport(selectCatalogs(args), holozip) + holotreeExport(selectCatalogs(args), nil, holozip) } pretty.Ok() }, @@ -79,6 +157,7 @@ var holotreeExportCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeExportCmd) + holotreeExportCmd.Flags().StringVarP(&specFile, "specification", "s", "", "Filename to use as export speficifaction in YAML format.") holotreeExportCmd.Flags().StringVarP(&holozip, "zipfile", "z", "hololib.zip", "Name of zipfile to export.") holotreeExportCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") holotreeExportCmd.Flags().StringVarP(&exportRobot, "robot", "r", "", "Full path to 'robot.yaml' configuration file to export as catalog. ") diff --git a/common/version.go b/common/version.go index 0a6619ae..d0424f61 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.32.6` + Version = `v11.33.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 54ea5329..7c602831 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.33.0 (date: 18.11.2022) UNSTABLE + +- feature: holotree delta export (for missing things only) +- changes normal holotree export command to support ".hld" files + ## v11.32.6 (date: 15.11.2022) - bugfix: from now on, lock pid files will only give diagnostic "warning" when diff --git a/htfs/functions.go b/htfs/functions.go index 8d8a771b..16e2e14b 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -463,9 +463,32 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat } type Zipper interface { + Ignore(relativepath string) Add(fullpath, relativepath string) error } +func ZipIgnore(library MutableLibrary, fs *Root, sink Zipper) Treetop { + var tool Treetop + baseline := common.HololibLocation() + tool = func(path string, it *Dir) (err error) { + defer fail.Around(&err) + + for _, file := range it.Files { + location := library.ExactLocation(file.Digest) + relative, err := filepath.Rel(baseline, location) + if err == nil { + sink.Ignore(relative) + } + } + for name, subdir := range it.Dirs { + err := tool(filepath.Join(path, name), subdir) + fail.On(err != nil, "%v", err) + } + return nil + } + return tool +} + func ZipRoot(library MutableLibrary, fs *Root, sink Zipper) Treetop { var tool Treetop baseline := common.HololibLocation() diff --git a/htfs/library.go b/htfs/library.go index 3556b7ea..6aea91a0 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -64,7 +64,7 @@ type MutableLibrary interface { Identity() string ExactLocation(string) string - Export([]string, string) error + Export([]string, []string, string) error Remove([]string) error Location(string) string Record([]byte) error @@ -109,6 +109,10 @@ type zipseen struct { seen map[string]bool } +func (it zipseen) Ignore(relativepath string) { + it.seen[relativepath] = true +} + func (it zipseen) Add(fullpath, relativepath string) (err error) { defer fail.Around(&err) @@ -145,7 +149,7 @@ func (it *hololib) Remove(catalogs []string) (err error) { return nil } -func (it *hololib) Export(catalogs []string, archive string) (err error) { +func (it *hololib) Export(catalogs, known []string, archive string) (err error) { defer fail.Around(&err) common.TimelineBegin("holotree export start") @@ -163,6 +167,18 @@ func (it *hololib) Export(catalogs []string, archive string) (err error) { exported := false + for _, name := range known { + catalog := filepath.Join(common.HololibCatalogLocation(), name) + fs, err := NewRoot(".") + fail.On(err != nil, "Could not create root location -> %v.", err) + err = fs.LoadFrom(catalog) + if err != nil { + continue + } + err = fs.Treetop(ZipIgnore(it, fs, zipper)) + fail.On(err != nil, "Could not ignore catalog %s -> %v.", catalog, err) + } + for _, name := range catalogs { catalog := filepath.Join(common.HololibCatalogLocation(), name) relative, err := filepath.Rel(common.HololibLocation(), catalog) diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index e2869c0e..5ac50d19 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -42,7 +42,7 @@ func (it *unmanaged) Remove([]string) error { return fmt.Errorf("Not supported yet on unmanaged holotree.") } -func (it *unmanaged) Export([]string, string) error { +func (it *unmanaged) Export([]string, []string, string) error { return fmt.Errorf("Not supported yet on unmanaged holotree.") } diff --git a/htfs/virtual.go b/htfs/virtual.go index 7c9e51d5..75f9e7f9 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -47,7 +47,7 @@ func (it *virtual) Remove([]string) error { return fmt.Errorf("Not supported yet on virtual holotree.") } -func (it *virtual) Export([]string, string) error { +func (it *virtual) Export([]string, []string, string) error { return fmt.Errorf("Not supported yet on virtual holotree.") } From 4ecaed308dca51f452bbaab33ceb123f920150d1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 23 Nov 2022 14:40:26 +0200 Subject: [PATCH 325/516] IMPROVEMENT: more timeline markers on assistant run (v11.33.1) - some additional timeline markers on assistant runs --- cmd/assistantRun.go | 3 +++ common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/assistant.go | 1 + operations/updownload.go | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index a44cdf83..315dae2c 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -24,6 +24,7 @@ var assistantRunCmd = &cobra.Command{ Short: "Robot Assistant run", Long: "Robot Assistant run.", Run: func(cmd *cobra.Command, args []string) { + common.Timeline("cmd/assistant run entered") defer conda.RemoveCurrentTemp() defer journal.BuildEventStats("assistant") var status, reason string @@ -36,10 +37,12 @@ var assistantRunCmd = &cobra.Command{ if account == nil { pretty.Exit(1, "Could not find account by name: %q", AccountName()) } + common.Timeline("new cloud client to %q", account.Endpoint) client, err := cloud.NewClient(account.Endpoint) if err != nil { pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } + common.Timeline("new cloud client created") reason = "START_FAILURE" cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.start", elapser.Elapsed().String()) defer func() { diff --git a/common/version.go b/common/version.go index d0424f61..4c398ff1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.33.0` + Version = `v11.33.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7c602831..ca70e3fa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.33.1 (date: 23.11.2022) UNSTABLE + +- some additional timeline markers on assistant runs + ## v11.33.0 (date: 18.11.2022) UNSTABLE - feature: holotree delta export (for missing things only) diff --git a/operations/assistant.go b/operations/assistant.go index f3c1ad76..140dfe43 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -310,6 +310,7 @@ func StartAssistantRun(client cloud.Client, account *account, workspaceId, assis if err != nil { return nil, err } + common.Timeline("start assistant run CR network request") request := client.NewRequest(fmt.Sprintf(startAssistantApi, workspaceId, assistantId)) request.Headers[authorization] = WorkspaceToken(credentials) request.Headers[contentType] = applicationJson diff --git a/operations/updownload.go b/operations/updownload.go index c4e092b3..423f4880 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -161,6 +161,7 @@ func DownloadCommand(client cloud.Client, account *account, workspaceId, robotId } func SummonRobotZipfile(client cloud.Client, account *account, workspaceId, robotId, digest string) (string, error) { + common.Timeline("summon networked/cached robot.zip") found, ok := LookupRobot(digest) if ok { return found, nil From 99c1e41815102cffc753e9477b7941076d467417 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 24 Nov 2022 11:03:51 +0200 Subject: [PATCH 326/516] IMPROVEMENT: DNS lookup time in diagnostics (v11.33.2) - configuration diagnostics now measure and report time it takes to resolve set of hostnames found from settings files --- common/elapsed.go | 20 ++++++++++++++------ common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 5 ++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/common/elapsed.go b/common/elapsed.go index e570d71b..81556809 100644 --- a/common/elapsed.go +++ b/common/elapsed.go @@ -54,19 +54,27 @@ func (it *stopwatch) Elapsed() Duration { } func (it *stopwatch) Debug() Duration { - elapsed := it.Elapsed() - Debug("%v %v", it.message, elapsed) + humane, elapsed := it.explained() + Debug(humane) return elapsed } func (it *stopwatch) Log() Duration { - elapsed := it.Elapsed() - Log("%v %v", it.message, elapsed) + humane, elapsed := it.explained() + Log(humane) return elapsed } func (it *stopwatch) Report() Duration { + return it.Log() +} + +func (it *stopwatch) Text() string { + humane, _ := it.explained() + return humane +} + +func (it *stopwatch) explained() (string, Duration) { elapsed := it.Elapsed() - Log("%v %v", it.message, elapsed) - return elapsed + return fmt.Sprintf("%s %ss", it.message, elapsed), elapsed } diff --git a/common/version.go b/common/version.go index 4c398ff1..002ff46b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.33.1` + Version = `v11.33.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index ca70e3fa..79d39ad9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.33.2 (date: 24.11.2022) + +- configuration diagnostics now measure and report time it takes to resolve + set of hostnames found from settings files + ## v11.33.1 (date: 23.11.2022) UNSTABLE - some additional timeline markers on assistant runs diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 9fd6da26..3a2952d5 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -126,9 +126,12 @@ func RunDiagnostics() *common.DiagnosticStatus { } result.Checks = append(result.Checks, lockpidsCheck()...) result.Checks = append(result.Checks, lockfilesCheck()...) - for _, host := range settings.Global.Hostnames() { + hostnames := settings.Global.Hostnames() + dnsStopwatch := common.Stopwatch("DNS lookup time for %d hostnames was about", len(hostnames)) + for _, host := range hostnames { result.Checks = append(result.Checks, dnsLookupCheck(host)) } + result.Details["dns-lookup-time"] = dnsStopwatch.Text() result.Checks = append(result.Checks, canaryDownloadCheck()) result.Checks = append(result.Checks, pypiHeadCheck()) result.Checks = append(result.Checks, condaHeadCheck()) From 5e7b564264419209677063b767f66d2932453c3e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 29 Nov 2022 11:24:55 +0200 Subject: [PATCH 327/516] FEATURE: arm support (v11.34.0) --- Rakefile | 23 ++++++++++++++++++- common/version.go | 2 +- ...orm_darwin_amd64.go => platform_darwin.go} | 0 ...tform_linux_amd64.go => platform_linux.go} | 0 ...m_windows_amd64.go => platform_windows.go} | 0 docs/changelog.md | 4 ++++ 6 files changed, 27 insertions(+), 2 deletions(-) rename conda/{platform_darwin_amd64.go => platform_darwin.go} (100%) rename conda/{platform_linux_amd64.go => platform_linux.go} (100%) rename conda/{platform_windows_amd64.go => platform_windows.go} (100%) diff --git a/Rakefile b/Rakefile index 96d163b5..086c9076 100644 --- a/Rakefile +++ b/Rakefile @@ -61,6 +61,13 @@ task :linux64 => [:what, :test] do sh "sha256sum build/linux64/* || true" end +task :linux64arm => [:what, :test] do + ENV['GOOS'] = 'linux' + ENV['GOARCH'] = 'arm64' + sh "go build -ldflags '-s' -o build/linux64/arm/ ./cmd/..." + sh "sha256sum build/linux64/arm/* || true" +end + task :macos64 => [:support] do ENV['GOOS'] = 'darwin' ENV['GOARCH'] = 'amd64' @@ -68,6 +75,13 @@ task :macos64 => [:support] do sh "sha256sum build/macos64/* || true" end +task :macos64arm => [:support] do + ENV['GOOS'] = 'darwin' + ENV['GOARCH'] = 'arm64' + sh "go build -ldflags '-s' -o build/macos64/arm/ ./cmd/..." + sh "sha256sum build/macos64/arm/* || true" +end + task :windows64 => [:support] do ENV['GOOS'] = 'windows' ENV['GOARCH'] = 'amd64' @@ -75,6 +89,13 @@ task :windows64 => [:support] do sh "sha256sum build/windows64/* || true" end +task :windows64arm => [:support] do + ENV['GOOS'] = 'windows' + ENV['GOARCH'] = 'arm64' + sh "go build -ldflags '-s' -o build/windows64/arm/ ./cmd/..." + sh "sha256sum build/windows64/arm/* || true" +end + desc 'Setup build environment' task :robotsetup do sh "#{PYTHON} -m pip install --upgrade -r robot_requirements.txt" @@ -92,7 +113,7 @@ task :robot => :local do end desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do +task :build => [:tooling, :version_txt, :linux64, :linux64arm, :macos64, :macos64arm, :windows64, :windows64arm] do sh 'ls -l $(find build -type f)' end diff --git a/common/version.go b/common/version.go index 002ff46b..96a0c726 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.33.2` + Version = `v11.34.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin.go similarity index 100% rename from conda/platform_darwin_amd64.go rename to conda/platform_darwin.go diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux.go similarity index 100% rename from conda/platform_linux_amd64.go rename to conda/platform_linux.go diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows.go similarity index 100% rename from conda/platform_windows_amd64.go rename to conda/platform_windows.go diff --git a/docs/changelog.md b/docs/changelog.md index 79d39ad9..d5ca6980 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.34.0 (date: 29.11.2022) + +- compiling rcc for arm64 architectures (linux, mac, windows) + ## v11.33.2 (date: 24.11.2022) - configuration diagnostics now measure and report time it takes to resolve From 3bee64ed5b3a3c4aff51b2425fda292a5b974b3b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Dec 2022 12:55:08 +0200 Subject: [PATCH 328/516] POC: starting peercc PoC (v11.35.0) - starting new PoC on topic of "peer rcc" - export specification simplification: now supports exactly one "wants" value and it is not list anymore, but just plain and simple string - added new "set" operations to support PoC functionality (generics) --- cmd/holotreeExport.go | 43 ++++------------- cmd/peercc/main.go | 77 ++++++++++++++++++++++++++++++ cmd/peercc/main_test.go | 1 + common/version.go | 2 +- docs/changelog.md | 8 ++++ htfs/export.go | 64 +++++++++++++++++++++++++ peercc/builder.go | 54 +++++++++++++++++++++ peercc/frontdesk.go | 70 +++++++++++++++++++++++++++ peercc/listings.go | 103 ++++++++++++++++++++++++++++++++++++++++ peercc/manage.go | 27 +++++++++++ peercc/messages.go | 24 ++++++++++ peercc/server.go | 71 +++++++++++++++++++++++++++ set/functions.go | 91 +++++++++++++++++++++++++++++++++++ set/functions_test.go | 44 +++++++++++++++++ 14 files changed, 645 insertions(+), 34 deletions(-) create mode 100644 cmd/peercc/main.go create mode 100644 cmd/peercc/main_test.go create mode 100644 htfs/export.go create mode 100644 peercc/builder.go create mode 100644 peercc/frontdesk.go create mode 100644 peercc/listings.go create mode 100644 peercc/manage.go create mode 100644 peercc/messages.go create mode 100644 peercc/server.go create mode 100644 set/functions.go create mode 100644 set/functions_test.go diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index 7b87a813..3f9c8ac1 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -11,7 +11,6 @@ import ( "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" ) var ( @@ -20,29 +19,12 @@ var ( specFile string ) -type ( - ExportSpec struct { - Workspace string `yaml:"workspace"` - Known []string `yaml:"knows"` - Needs []string `yaml:"wants"` - } -) - -func specFrom(content []byte) (*ExportSpec, error) { - result := &ExportSpec{} - err := yaml.Unmarshal(content, result) - if err != nil { - return nil, err - } - return result, nil -} - -func loadExportSpec(filename string) (*ExportSpec, error) { +func loadExportSpec(filename string) (*htfs.ExportSpec, error) { raw, err := os.ReadFile(filename) if err != nil { return nil, err } - spec, err := specFrom(raw) + spec, err := htfs.ParseExportSpec(raw) if err != nil { return nil, err } @@ -52,20 +34,15 @@ func loadExportSpec(filename string) (*ExportSpec, error) { func exportBySpecification(filename string) { spec, err := loadExportSpec(filename) pretty.Guard(err == nil, 4, "Loading specification %q failed, reason: %v", filename, err) - known := selectExactCatalogs(spec.Known) - needs := selectExactCatalogs(spec.Needs) - pretty.Guard(len(spec.Needs) == len(needs), 5, "Only %d out of %d needed catalogs available. Quitting!", len(needs), len(spec.Needs)) - unifiedSpec := &ExportSpec{ - Workspace: spec.Workspace, - Known: known, - Needs: needs, - } - content, err := yaml.Marshal(unifiedSpec) - pretty.Guard(err == nil, 6, "Marshaling unified specification failed, reason: %v", err) - fingerprint := common.Siphash(9007199254740993, 2147483647, content) - common.Debug("Final delta specification %0x16x is:\n%s", fingerprint, string(content)) + known := selectExactCatalogs(spec.Knows) + wants := selectExactCatalogs([]string{spec.Wants}) + pretty.Guard(len(wants) == 1, 5, "Only %d out of 1 needed catalogs available. Quitting!", len(wants)) + unifiedSpec := htfs.NewExportSpec(spec.Domain, spec.Wants, known) + textual, fingerprint, err := unifiedSpec.Fingerprint() + pretty.Guard(err == nil, 6, "Fingerprinting unified specification failed, reason: %v", err) + common.Debug("Final delta specification %0x16x is:\n%s", fingerprint, textual) deltafile := fmt.Sprintf("%016x.hld", fingerprint) - holotreeExport(needs, known, deltafile) + holotreeExport(wants, unifiedSpec.Knows, deltafile) common.Stdout("%s\n", deltafile) } diff --git a/cmd/peercc/main.go b/cmd/peercc/main.go new file mode 100644 index 00000000..078b4566 --- /dev/null +++ b/cmd/peercc/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/peercc" + "github.com/robocorp/rcc/pretty" +) + +var ( + domainId string + serverName string + serverPort int + versionFlag bool + holdingArea string +) + +func defaultHoldLocation() string { + where, err := pathlib.Abs(filepath.Join(os.TempDir(), "peercchold")) + if err != nil { + return "temphold" + } + return where +} + +func init() { + flag.BoolVar(&common.DebugFlag, "debug", false, "Turn on debugging output.") + flag.BoolVar(&common.TraceFlag, "trace", false, "Turn on tracing output.") + + flag.BoolVar(&versionFlag, "version", false, "Just show peercc version and exit.") + flag.StringVar(&serverName, "hostname", "localhost", "Hostname/address to bind server to.") + flag.IntVar(&serverPort, "port", 4653, "Port to bind server in given hostname.") + flag.StringVar(&holdingArea, "hold", defaultHoldLocation(), "Directory where to put HOLD files once known.") + flag.StringVar(&domainId, "domain", "personal", "Symbolic domain that this peer serves.") +} + +func ExitProtection() { + status := recover() + if status != nil { + exit, ok := status.(common.ExitCode) + if ok { + exit.ShowMessage() + common.WaitLogs() + os.Exit(exit.Code) + } + common.WaitLogs() + panic(status) + } + common.WaitLogs() +} + +func showVersion() { + common.Stdout("%s\n", common.Version) + os.Exit(0) +} + +func process() { + if versionFlag { + showVersion() + } + pretty.Guard(common.SharedHolotree, 1, "Shared holotree must be enabled and in use for peercc to work.") + common.Log("Peer for rcc starting (%s) ...", common.Version) + peercc.Serve(serverName, serverPort, domainId, holdingArea) +} + +func main() { + defer ExitProtection() + pretty.Setup() + + flag.Parse() + common.UnifyVerbosityFlags() + process() +} diff --git a/cmd/peercc/main_test.go b/cmd/peercc/main_test.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/cmd/peercc/main_test.go @@ -0,0 +1 @@ +package main diff --git a/common/version.go b/common/version.go index 96a0c726..6bad7b22 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.34.0` + Version = `v11.35.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index d5ca6980..c2267235 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v11.35.0 (date: 7.12.2022) UNSTABLE + +- starting new PoC on topic of "peer rcc" +- export specification simplification: now supports exactly one "wants" value + and it is not list anymore, but just plain and simple string +- added new "set" operations to support PoC functionality (generics) +- one part of PoC failed, but code is still there + ## v11.34.0 (date: 29.11.2022) - compiling rcc for arm64 architectures (linux, mac, windows) diff --git a/htfs/export.go b/htfs/export.go new file mode 100644 index 00000000..a13edec3 --- /dev/null +++ b/htfs/export.go @@ -0,0 +1,64 @@ +package htfs + +import ( + "fmt" + "sort" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/set" + "gopkg.in/yaml.v2" +) + +type ( + ExportSpec struct { + Domain string `yaml:"domain"` + Wants string `yaml:"wants"` + Knows []string `yaml:"knows"` + } +) + +func (it *ExportSpec) IsRoot() bool { + return len(it.Knows) == 0 +} + +func (it *ExportSpec) RootSpec() *ExportSpec { + if it.IsRoot() { + return it + } + return NewExportSpec(it.Domain, it.Wants, []string{}) +} + +func (it *ExportSpec) HoldName() string { + _, fingerprint, err := it.Fingerprint() + if err != nil { + return "broken.hld" + } + return fmt.Sprintf("%016x.hld", fingerprint) +} + +func (it *ExportSpec) Fingerprint() (string, uint64, error) { + sort.Strings(it.Knows) + content, err := yaml.Marshal(it) + if err != nil { + return "", 0, err + } + fingerprint := common.Siphash(9007199254740993, 2147483647, content) + return string(content), fingerprint, nil +} + +func ParseExportSpec(content []byte) (*ExportSpec, error) { + result := &ExportSpec{} + err := yaml.Unmarshal(content, result) + if err != nil { + return nil, err + } + return result, nil +} + +func NewExportSpec(domain, wants string, knows []string) *ExportSpec { + return &ExportSpec{ + Domain: domain, + Wants: wants, + Knows: set.Set(knows), + } +} diff --git a/peercc/builder.go b/peercc/builder.go new file mode 100644 index 00000000..25401268 --- /dev/null +++ b/peercc/builder.go @@ -0,0 +1,54 @@ +package peercc + +import ( + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pathlib" +) + +func feedInitialSpecs(domain string, specs Specs) { + for _, catalog := range htfs.Catalogs() { + specs <- htfs.NewExportSpec(domain, catalog, []string{}) + } +} + +func buildSpecToStorage(storage string, spec *htfs.ExportSpec) (string, bool) { + holdfile := spec.HoldName() + + defer common.Stopwatch("Build of spec %q -> %q took", spec.Wants, holdfile).Debug() + tree, err := htfs.New() + if err != nil { + return "", false + } + archive := filepath.Join(storage, holdfile) + err = tree.Export([]string{spec.Wants}, spec.Knows, archive) + if err != nil { + return "", false + } + return holdfile, true +} + +func builder(storage string, specs Specs, catalogs Catalogs, holds Holdfiles) { + common.Debug("Builder for %q starting ...", storage) + pathlib.EnsureDirectoryExists(storage) +forever: + for { + todo, ok := <-specs + if !ok { + break forever + } + if todo == nil { + continue + } + common.Debug("Build for %q requested.", todo.HoldName()) + holdfile, ok := buildSpecToStorage(storage, todo) + if ok { + catalogs <- todo.Wants + holds <- holdfile + common.Debug("Build for %q done.", todo.HoldName()) + } + } + common.Debug("Builder stopped!") +} diff --git a/peercc/frontdesk.go b/peercc/frontdesk.go new file mode 100644 index 00000000..51c18790 --- /dev/null +++ b/peercc/frontdesk.go @@ -0,0 +1,70 @@ +package peercc + +import ( + "fmt" + "net/url" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/set" +) + +func knownSpec(knowledge []string, spec *htfs.ExportSpec) (*htfs.ExportSpec, *htfs.ExportSpec) { + if !set.Member(knowledge, spec.Wants) { + return nil, nil + } + delta := htfs.NewExportSpec(spec.Domain, spec.Wants, set.Intersect(knowledge, spec.Knows)) + if delta.IsRoot() { + return nil, delta + } + return delta, delta.RootSpec() +} + +func processQuery(available []string, query *Query) *htfs.ExportSpec { + defer close(query.Reply) + + delta, root := knownSpec(available, query.Specification) + if root == nil && delta == nil { + return nil + } + selected := delta + if selected == nil { + selected = root + } + link, err := url.Parse(fmt.Sprintf("/hold/%s", selected.HoldName())) + if err != nil { + return delta + } + query.Reply <- link + return delta +} + +func frontdesk(catalogs Catalogs, holds Holdfiles, queries Queries, specs Specs) { + common.Debug("Frontdesk starting ...") + available := []string{} + holding := []string{} +forever: + for { + select { + case catalog, ok := <-catalogs: + if !ok { + break forever + } + available, _ = set.Update(available, catalog) + case hold, ok := <-holds: + if !ok { + break forever + } + holding, _ = set.Update(holding, hold) + case query, ok := <-queries: + if !ok { + break forever + } + delta := processQuery(available, query) + if delta != nil { + specs <- delta + } + } + } + common.Debug("Frontdesk stopping!") +} diff --git a/peercc/listings.go b/peercc/listings.go new file mode 100644 index 00000000..1aef615e --- /dev/null +++ b/peercc/listings.go @@ -0,0 +1,103 @@ +package peercc + +import ( + "bufio" + "net/http" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/set" +) + +const ( + partCacheSize = 20 +) + +func makeQueryHandler(queries Partqueries) http.HandlerFunc { + return func(response http.ResponseWriter, request *http.Request) { + catalog := filepath.Base(request.URL.Path) + defer common.Stopwatch("Query of catalog %q took", catalog).Debug() + reply := make(chan string) + queries <- &Partquery{ + Catalog: catalog, + Reply: reply, + } + content, ok := <-reply + common.Debug("query handler: %q -> %v", catalog, ok) + if !ok { + response.WriteHeader(http.StatusNotFound) + response.Write([]byte("404 not found, sorry")) + return + } + headers := response.Header() + headers.Add("Content-Type", "text/plain") + response.WriteHeader(http.StatusOK) + writer := bufio.NewWriter(response) + defer writer.Flush() + writer.WriteString(content) + } +} + +func loadSingleCatalog(catalog string) (root *htfs.Root, err error) { + defer fail.Around(&err) + tempdir := filepath.Join(common.RobocorpTemp(), "peercc") + shadow, err := htfs.NewRoot(tempdir) + fail.On(err != nil, "Could not create root, reason: %v", err) + filename := filepath.Join(common.HololibCatalogLocation(), catalog) + err = shadow.LoadFrom(filename) + fail.On(err != nil, "Could not load root, reason: %v", err) + common.Trace("Catalog %q loaded.", catalog) + return shadow, nil +} + +func loadCatalogParts(catalog string) (string, bool) { + catalogs := htfs.Catalogs() + if !set.Member(catalogs, catalog) { + return "", false + } + root, err := loadSingleCatalog(catalog) + if err != nil { + return "", false + } + collector := make(map[string]string) + task := htfs.DigestMapper(collector) + err = task(root.Path, root.Tree) + if err != nil { + return "", false + } + keys := set.Keys(collector) + return strings.Join(keys, "\n"), true +} + +func listProvider(queries Partqueries) { + cache := make(map[string]string) + keys := make([]string, partCacheSize) + cursor := uint64(0) +loop: + for { + query, ok := <-queries + if !ok { + break loop + } + known, ok := cache[query.Catalog] + if ok { + query.Reply <- known + close(query.Reply) + continue + } + created, ok := loadCatalogParts(query.Catalog) + if !ok { + close(query.Reply) + continue + } + delete(cache, keys[cursor%partCacheSize]) + cache[query.Catalog] = created + keys[cursor] = query.Catalog + cursor += 1 + query.Reply <- created + close(query.Reply) + } +} diff --git a/peercc/manage.go b/peercc/manage.go new file mode 100644 index 00000000..61b491cc --- /dev/null +++ b/peercc/manage.go @@ -0,0 +1,27 @@ +package peercc + +import ( + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pathlib" +) + +func cleanupHoldStorage(storage string) error { + if !pathlib.IsDir(storage) { + return nil + } + filenames, err := filepath.Glob(filepath.Join(storage, "*.hld")) + if err != nil { + return err + } + for _, filename := range filenames { + err = htfs.TryRemove("hold", filename) + if err != nil { + return err + } + common.Debug("Old hold file %q removed.", filename) + } + return nil +} diff --git a/peercc/messages.go b/peercc/messages.go new file mode 100644 index 00000000..52cb9684 --- /dev/null +++ b/peercc/messages.go @@ -0,0 +1,24 @@ +package peercc + +import ( + "net/url" + + "github.com/robocorp/rcc/htfs" +) + +type ( + URLs chan *url.URL + Query struct { + Specification *htfs.ExportSpec + Reply URLs + } + Queries chan *Query + Catalogs chan string + Holdfiles chan string + Specs chan *htfs.ExportSpec + Partquery struct { + Catalog string + Reply chan string + } + Partqueries chan *Partquery +) diff --git a/peercc/server.go b/peercc/server.go new file mode 100644 index 00000000..d40ef5dc --- /dev/null +++ b/peercc/server.go @@ -0,0 +1,71 @@ +package peercc + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" +) + +func stopper(server *http.Server) error { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) + <-signals + return server.Shutdown(context.TODO()) +} + +func Serve(address string, port int, domain, storage string) error { + // we need + // - builder + // - holder + // - webserver + // - download handler (for optimists) + // - specification handler (for pessimists) + holding := filepath.Join(storage, "hold") + err := cleanupHoldStorage(holding) + if err != nil { + return err + } + defer cleanupHoldStorage(holding) + + holds := make(Holdfiles) + specs := make(Specs, 30) + queries := make(Queries) + catalogs := make(Catalogs) + partqueries := make(Partqueries) + signals := make(chan os.Signal, 1) + + signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) + + defer close(holds) + defer close(specs) + defer close(queries) + defer close(catalogs) + + go listProvider(partqueries) + //go feedInitialSpecs(domain, specs) + //go frontdesk(catalogs, holds, queries, specs) + //go builder(holding, specs, catalogs, holds) + + listen := fmt.Sprintf("%s:%d", address, port) + mux := http.NewServeMux() + server := &http.Server{ + Addr: listen, + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 14, + } + + mux.HandleFunc("/parts/", makeQueryHandler(partqueries)) + + go server.ListenAndServe() + + <-signals + + return server.Shutdown(context.TODO()) +} diff --git a/set/functions.go b/set/functions.go new file mode 100644 index 00000000..c1cd9e7f --- /dev/null +++ b/set/functions.go @@ -0,0 +1,91 @@ +package set + +import "sort" + +type ( + comparable interface { + string | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 + } +) + +func With[T comparable](incoming ...T) []T { + return Set(incoming) +} + +func Set[T comparable](incoming []T) []T { + return Keys(itemset(incoming)) +} + +func Values[Key, Value comparable](incoming map[Key]Value) []Value { + intermediate := make(map[Value]bool) + for _, value := range incoming { + intermediate[value] = true + } + return Keys(intermediate) +} + +func Keys[Key comparable, Value any](incoming map[Key]Value) []Key { + result := make([]Key, 0, len(incoming)) + for key, _ := range incoming { + result = append(result, key) + } + return Sort(result) +} + +func Sort[T comparable](set []T) []T { + sort.Slice(set, func(left, right int) bool { + return set[left] < set[right] + }) + return set +} + +func Member[T comparable](set []T, candidate T) bool { + for _, item := range set { + if candidate == item { + return true + } + } + return false +} + +func Update[T comparable](set []T, candidate T) ([]T, bool) { + if Member(set, candidate) { + return set, false + } + return Sort(append(set, candidate)), true +} + +func Intersect[T comparable](left, right []T) []T { + if len(right) < len(left) { + left, right = right, left + } + missing := len(left) + checked := itemset(left) + intermediate := make(map[T]bool) + for _, candidate := range right { + if checked[candidate] { + intermediate[candidate] = true + missing-- + } + if missing == 0 { + break + } + } + return Keys(intermediate) +} + +func Union[T comparable](left, right []T) []T { + intermediate := itemset(left) + for _, item := range right { + intermediate[item] = true + } + return Keys(intermediate) +} + +func itemset[T comparable](items []T) map[T]bool { + result := make(map[T]bool) + for _, item := range items { + result[item] = true + } + return result +} diff --git a/set/functions_test.go b/set/functions_test.go new file mode 100644 index 00000000..40ce1ba3 --- /dev/null +++ b/set/functions_test.go @@ -0,0 +1,44 @@ +package set_test + +import ( + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/set" +) + +func TestMakingAndAppending(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + original := set.Set([]string{"B", "A", "A", "B", "B", "A"}) + must_be.Text("[A B]", original) + + updated, ok := set.Update(original, "C") + must_be.True(ok) + must_be.Text("[A B C]", updated) + + already, ok := set.Update([]string{"A", "B", "C"}, "C") + wont_be.True(ok) + must_be.Text("[A B C]", already) +} + +func TestMembershipAndSorting(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + original := []string{"B", "A", "D", "F", "E", "C"} + must_be.True(set.Member(original, "F")) + wont_be.True(set.Member(original, "G")) + must_be.Text("[A B C D E F]", set.Sort(original)) + must_be.Text("[A B C D E F]", original) +} + +func TestOperations(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + smaller := set.With("D", "E", "F") + bigger := set.With("F", "A", "C", "E", "C", "A", "P") + must_be.True(set.Member(smaller, "D")) + wont_be.True(set.Member(bigger, "D")) + must_be.Text("[E F]", set.Intersect(smaller, bigger)) + must_be.Text("[A C D E F P]", set.Union(smaller, bigger)) +} From e88ce319b91db86bc11bfcb15756394b0e42502e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Dec 2022 14:21:45 +0200 Subject: [PATCH 329/516] GHA: upgrade GHA to use ruby 2.7 (v11.35.1) --- .github/workflows/rcc.yaml | 4 ++-- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index 10d94223..40f6f434 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -18,7 +18,7 @@ jobs: go-version: '1.18.x' - uses: actions/setup-ruby@v1 with: - ruby-version: '2.5' + ruby-version: '2.7' - uses: actions/checkout@v1 - name: What run: rake what @@ -38,7 +38,7 @@ jobs: go-version: '1.18.x' - uses: actions/setup-ruby@v1 with: - ruby-version: '2.5' + ruby-version: '2.7' - uses: actions/setup-python@v1 with: python-version: '3.7' diff --git a/common/version.go b/common/version.go index 6bad7b22..cab0da25 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.0` + Version = `v11.35.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index c2267235..2db06d54 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.35.1 (date: 7.12.2022) UNSTABLE + +- github actions updated to use ruby 2.7 (github stopped supporting used 2.5) + ## v11.35.0 (date: 7.12.2022) UNSTABLE - starting new PoC on topic of "peer rcc" From 13a822cf33b1f27c9f042085ec6d330f5d1d8073 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Dec 2022 14:27:54 +0200 Subject: [PATCH 330/516] GHA: upgrade GHA to use ruby 2.7 (v11.35.2) --- .github/workflows/rcc.yaml | 4 ++-- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index 40f6f434..c62cbf06 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: '1.18.x' - - uses: actions/setup-ruby@v1 + - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' - uses: actions/checkout@v1 @@ -36,7 +36,7 @@ jobs: - uses: actions/setup-go@v2 with: go-version: '1.18.x' - - uses: actions/setup-ruby@v1 + - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' - uses: actions/setup-python@v1 diff --git a/common/version.go b/common/version.go index cab0da25..f936d62c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.1` + Version = `v11.35.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 2db06d54..0f4afca3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.35.2 (date: 7.12.2022) UNSTABLE + +- next try to fix ruby support in GHA + ## v11.35.1 (date: 7.12.2022) UNSTABLE - github actions updated to use ruby 2.7 (github stopped supporting used 2.5) From 08fd93bd3ca075b5f3a6545126851f129d6a4e38 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Dec 2022 16:26:26 +0200 Subject: [PATCH 331/516] BUGFIX: ioutil deprecations and diagnostics (v11.35.3) - replaced deprecated "ioutil" with suitable functions elsewhere, thank you for Juneezee (Eng Zer Jun) for pointing these out in PR#40 - added ComSpec, LANG and SHELL from environment into diagnostics output --- cloud/client.go | 3 +-- cmd/rcc/main.go | 3 +-- common/version.go | 2 +- conda/activate.go | 8 ++++---- conda/condayaml.go | 8 ++++---- conda/config.go | 4 ++-- conda/workflows.go | 7 +++---- docs/changelog.md | 6 ++++++ htfs/commands.go | 3 +-- operations/assistant.go | 3 +-- operations/authorize_test.go | 4 ++-- operations/diagnostics.go | 6 ++++-- operations/fixing.go | 5 ++--- operations/issues.go | 3 +-- pathlib/walk.go | 3 +-- peercc/server.go | 12 +----------- robot/robot.go | 3 +-- robot/setup.go | 4 ++-- settings/settings.go | 3 +-- 19 files changed, 39 insertions(+), 51 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index 35e27b1d..86236e47 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -4,7 +4,6 @@ import ( "crypto/sha256" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -133,7 +132,7 @@ func (it *internalClient) does(method string, request *Request) *Response { if request.Stream != nil { io.Copy(request.Stream, httpResponse.Body) } else { - response.Body, response.Err = ioutil.ReadAll(httpResponse.Body) + response.Body, response.Err = io.ReadAll(httpResponse.Body) } if common.DebugFlag { body := "ignore" diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 6a9f3509..73722b74 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" "runtime" @@ -85,7 +84,7 @@ func markTempForRecycling() { target := common.RobocorpTempName() if pathlib.Exists(target) { filename := filepath.Join(target, "recycle.now") - ioutil.WriteFile(filename, []byte("True"), 0o644) + os.WriteFile(filename, []byte("True"), 0o644) common.Debug("Marked %q for recycling.", target) markedAlready = true } diff --git a/common/version.go b/common/version.go index f936d62c..acae9052 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.2` + Version = `v11.35.3` ) diff --git a/conda/activate.go b/conda/activate.go index 7f7779a2..217aef28 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -6,7 +6,7 @@ import ( "fmt" "html/template" "io" - "io/ioutil" + "os" "path/filepath" "strings" @@ -62,7 +62,7 @@ func createScript(targetFolder string) (string, error) { script.Execute(buffer, details) scriptfile := filepath.Join(targetFolder, fmt.Sprintf("rcc_activate%s", commandSuffix)) - err = ioutil.WriteFile(scriptfile, buffer.Bytes(), 0o755) + err = os.WriteFile(scriptfile, buffer.Bytes(), 0o755) if err != nil { return "", err } @@ -138,7 +138,7 @@ func Activate(sink io.Writer, targetFolder string) error { return err } targetJson := filepath.Join(targetFolder, activateFile) - err = ioutil.WriteFile(targetJson, body, 0o644) + err = os.WriteFile(targetJson, body, 0o644) if err != nil { return err } @@ -148,7 +148,7 @@ func Activate(sink io.Writer, targetFolder string) error { func LoadActivationEnvironment(targetFolder string) []string { result := []string{} targetJson := filepath.Join(targetFolder, activateFile) - content, err := ioutil.ReadFile(targetJson) + content, err := os.ReadFile(targetJson) if err != nil { return result } diff --git a/conda/condayaml.go b/conda/condayaml.go index c7734439..3631de20 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -2,7 +2,7 @@ package conda import ( "fmt" - "io/ioutil" + "os" "regexp" "strings" @@ -429,13 +429,13 @@ func (it *Environment) SaveAs(filename string) error { return err } common.Trace("FINAL conda environment file as %v:\n---\n%v---", filename, content) - return ioutil.WriteFile(filename, []byte(content), 0o640) + return os.WriteFile(filename, []byte(content), 0o640) } func (it *Environment) SaveAsRequirements(filename string) error { content := it.AsRequirementsText() common.Trace("FINAL pip requirements as %v:\n---\n%v\n---", filename, content) - return ioutil.WriteFile(filename, []byte(content), 0o640) + return os.WriteFile(filename, []byte(content), 0o640) } func (it *Environment) AsYaml() (string, error) { @@ -550,7 +550,7 @@ func CondaYamlFrom(content []byte) (*Environment, error) { } func ReadCondaYaml(filename string) (*Environment, error) { - content, err := ioutil.ReadFile(filename) + content, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } diff --git a/conda/config.go b/conda/config.go index 288b8baa..98abf002 100644 --- a/conda/config.go +++ b/conda/config.go @@ -1,7 +1,7 @@ package conda import ( - "io/ioutil" + "os" "regexp" "sort" "strings" @@ -20,7 +20,7 @@ func SplitLines(value string) []string { } func ReadConfig(filename string) (string, error) { - content, err := ioutil.ReadFile(filename) + content, err := os.ReadFile(filename) if err != nil { return "", err } diff --git a/conda/workflows.go b/conda/workflows.go index a60a2c36..be1e8eed 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "math/rand" "os" "path/filepath" @@ -130,7 +129,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } defer func() { planSink.Close() - content, err := ioutil.ReadFile(planfile) + content, err := os.ReadFile(planfile) if err == nil { common.Log("%s", string(content)) } @@ -273,7 +272,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") - err = ioutil.WriteFile(markerFile, []byte(yaml), 0o644) + err = os.WriteFile(markerFile, []byte(yaml), 0o644) if err != nil { return false, false } @@ -282,7 +281,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if ok { venvContent := fmt.Sprintf(venvTemplate, targetFolder, pythonVersionAt(targetFolder)) venvFile := filepath.Join(targetFolder, "pyvenv.cfg") - err = ioutil.WriteFile(venvFile, []byte(venvContent), 0o644) + err = os.WriteFile(venvFile, []byte(venvContent), 0o644) if err != nil { return false, false } diff --git a/docs/changelog.md b/docs/changelog.md index 0f4afca3..855538a9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.35.3 (date: 7.12.2022) UNSTABLE + +- replaced deprecated "ioutil" with suitable functions elsewhere, thank you + for Juneezee (Eng Zer Jun) for pointing these out in PR#40 +- added ComSpec, LANG and SHELL from environment into diagnostics output + ## v11.35.2 (date: 7.12.2022) UNSTABLE - next try to fix ruby support in GHA diff --git a/htfs/commands.go b/htfs/commands.go index fe6b656d..e506f06f 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -1,7 +1,6 @@ package htfs import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -135,7 +134,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec common.Progress(4, "Build environment into holotree stage.") identityfile := filepath.Join(tree.Stage(), "identity.yaml") - err = ioutil.WriteFile(identityfile, blueprint, 0o644) + err = os.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) err = conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) diff --git a/operations/assistant.go b/operations/assistant.go index 140dfe43..47d0155e 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" "net/url" @@ -202,7 +201,7 @@ func MultipartUpload(url string, fields map[string]string, basename, fullpath st } func IoAsString(source io.Reader) string { - body, err := ioutil.ReadAll(source) + body, err := io.ReadAll(source) if err != nil { return "" } diff --git a/operations/authorize_test.go b/operations/authorize_test.go index 646440d5..d5e18e6d 100644 --- a/operations/authorize_test.go +++ b/operations/authorize_test.go @@ -2,7 +2,7 @@ package operations_test import ( "fmt" - "io/ioutil" + "io" "strings" "testing" @@ -45,7 +45,7 @@ func TestBodyIsCorrectlyConverted(t *testing.T) { reader := strings.NewReader("{\n}") wont_be.Nil(reader) - body, err := ioutil.ReadAll(reader) + body, err := io.ReadAll(reader) must_be.Nil(err) wont_be.Nil(body) must_be.Equal("{\n}", string(body)) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 3a2952d5..769aad6b 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net" "os" "os/user" @@ -99,6 +98,9 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") result.Details["no-build"] = fmt.Sprintf("%v", settings.Global.NoBuild()) + result.Details["ENV:ComSpec"] = os.Getenv("ComSpec") + result.Details["ENV:SHELL"] = os.Getenv("SHELL") + result.Details["ENV:LANG"] = os.Getenv("LANG") for name, filename := range lockfiles() { result.Details[name] = filename @@ -473,7 +475,7 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str if shouldIgnorePath(fullpath) { continue } - content, err := ioutil.ReadFile(fullpath) + content, err := os.ReadFile(fullpath) if err != nil { diagnose.Fail(supportGeneralUrl, "Problem reading %s file %q: %v", label, tail, err) success = false diff --git a/operations/fixing.go b/operations/fixing.go index 27c4ffe2..12e86a71 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -2,7 +2,6 @@ package operations import ( "bytes" - "io/ioutil" "os" "path/filepath" "strings" @@ -40,12 +39,12 @@ func ToUnix(content []byte) []byte { } func fixShellFile(fullpath string) { - content, err := ioutil.ReadFile(fullpath) + content, err := os.ReadFile(fullpath) if err != nil || bytes.IndexByte(content, '\r') < 0 { return } common.Debug("Fixing newlines in file: %v", fullpath) - err = ioutil.WriteFile(fullpath, ToUnix(content), 0o755) + err = os.WriteFile(fullpath, ToUnix(content), 0o755) if err != nil { common.Log("Failure %v while fixing newlines in %v!", err, fullpath) } diff --git a/operations/issues.go b/operations/issues.go index 0052c753..bcf5f482 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -3,7 +3,6 @@ package operations import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" @@ -19,7 +18,7 @@ const ( ) func loadToken(reportFile string) (Token, error) { - content, err := ioutil.ReadFile(reportFile) + content, err := os.ReadFile(reportFile) if err != nil { return nil, err } diff --git a/pathlib/walk.go b/pathlib/walk.go index d009cf27..6dc21f4b 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -1,7 +1,6 @@ package pathlib import ( - "io/ioutil" "os" "path/filepath" "sort" @@ -93,7 +92,7 @@ func IgnorePattern(text string) Ignore { } func LoadIgnoreFile(filename string) (Ignore, error) { - content, err := ioutil.ReadFile(filename) + content, err := os.ReadFile(filename) if err != nil { return nil, err } diff --git a/peercc/server.go b/peercc/server.go index d40ef5dc..11deb8db 100644 --- a/peercc/server.go +++ b/peercc/server.go @@ -32,24 +32,14 @@ func Serve(address string, port int, domain, storage string) error { } defer cleanupHoldStorage(holding) - holds := make(Holdfiles) - specs := make(Specs, 30) - queries := make(Queries) - catalogs := make(Catalogs) partqueries := make(Partqueries) signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) - defer close(holds) - defer close(specs) - defer close(queries) - defer close(catalogs) + defer close(partqueries) go listProvider(partqueries) - //go feedInitialSpecs(domain, specs) - //go frontdesk(catalogs, holds, queries, specs) - //go builder(holding, specs, catalogs, holds) listen := fmt.Sprintf("%s:%d", address, port) mux := http.NewServeMux() diff --git a/robot/robot.go b/robot/robot.go index fdb503c7..40653d4e 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -3,7 +3,6 @@ package robot import ( "errors" "fmt" - "io/ioutil" "os" "path/filepath" "regexp" @@ -529,7 +528,7 @@ func LoadRobotYaml(filename string, visible bool) (Robot, error) { if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } - content, err := ioutil.ReadFile(fullpath) + content, err := os.ReadFile(fullpath) if err != nil { return nil, fmt.Errorf("%q: %w", fullpath, err) } diff --git a/robot/setup.go b/robot/setup.go index f52e3f0f..f86d9094 100644 --- a/robot/setup.go +++ b/robot/setup.go @@ -2,7 +2,7 @@ package robot import ( "fmt" - "io/ioutil" + "os" "path/filepath" "gopkg.in/yaml.v2" @@ -38,7 +38,7 @@ func LoadEnvironmentSetup(filename string) (Setup, error) { if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } - content, err := ioutil.ReadFile(fullpath) + content, err := os.ReadFile(fullpath) if err != nil { return nil, fmt.Errorf("%q: %w", fullpath, err) } diff --git a/settings/settings.go b/settings/settings.go index dd9c4e65..48937aa8 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -61,7 +60,7 @@ func CustomSettingsLayer() *Settings { } func LoadSetting(filename string) (*Settings, error) { - content, err := ioutil.ReadFile(filename) + content, err := os.ReadFile(filename) if err != nil { return nil, err } From 7a2dfc9c716040bd78a563249555a4d79d1ce317 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 8 Dec 2022 12:05:06 +0200 Subject: [PATCH 332/516] POC: continued on peercc PoC (v11.35.4) - removing failed parts of PoC - added handler for streaming of requested catalog and missing parts - made robot tests to automatically disconnect from shared holotree --- common/version.go | 2 +- docs/changelog.md | 6 +++ htfs/library.go | 12 ++++- operations/zipper.go | 25 ++++++----- peercc/builder.go | 54 ---------------------- peercc/delta.go | 90 +++++++++++++++++++++++++++++++++++++ peercc/frontdesk.go | 70 ----------------------------- peercc/listings.go | 5 +++ peercc/messages.go | 15 ------- peercc/missing_test.go | 1 + peercc/server.go | 28 +++++------- robot_tests/resources.robot | 3 ++ 12 files changed, 142 insertions(+), 169 deletions(-) delete mode 100644 peercc/builder.go create mode 100644 peercc/delta.go delete mode 100644 peercc/frontdesk.go create mode 100644 peercc/missing_test.go diff --git a/common/version.go b/common/version.go index acae9052..18842acf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.3` + Version = `v11.35.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 855538a9..703d7520 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.35.4 (date: 8.12.2022) UNSTABLE + +- removing failed parts of PoC +- added handler for streaming of requested catalog and missing parts +- made robot tests to automatically disconnect from shared holotree + ## v11.35.3 (date: 7.12.2022) UNSTABLE - replaced deprecated "ioutil" with suitable functions elsewhere, thank you diff --git a/htfs/library.go b/htfs/library.go index 6aea91a0..b862f477 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -86,10 +86,20 @@ func (it *hololib) Location(digest string) string { return filepath.Join(common.HololibLibraryLocation(), digest[:2], digest[2:4], digest[4:6]) } -func (it *hololib) ExactLocation(digest string) string { +func ExactDefaultLocation(digest string) string { return filepath.Join(common.HololibLibraryLocation(), digest[:2], digest[2:4], digest[4:6], digest) } +func RelativeDefaultLocation(digest string) string { + location := ExactDefaultLocation(digest) + relative, _ := filepath.Rel(common.HololibLocation(), location) + return relative +} + +func (it *hololib) ExactLocation(digest string) string { + return ExactDefaultLocation(digest) +} + func (it *hololib) Identity() string { suffix := fmt.Sprintf("%016x", it.identity) return fmt.Sprintf("h%s_%st", common.UserHomeIdentity(), suffix[:14]) diff --git a/operations/zipper.go b/operations/zipper.go index 797cdc53..d06c967a 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -204,24 +204,27 @@ func (it *zipper) Note(err error) { common.Debug("Warning! %v", err) } -func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { - if details != nil { - common.Debug("- %v size %v", relativepath, details.Size()) - } else { - common.Debug("- %v", relativepath) - } +func ZipAppend(writer *zip.Writer, fullpath, relativepath string) error { source, err := os.Open(fullpath) if err != nil { - it.Note(err) - return + return err } defer source.Close() - target, err := it.writer.Create(slashed(relativepath)) + target, err := writer.Create(slashed(relativepath)) if err != nil { - it.Note(err) - return + return err } _, err = io.Copy(target, source) + return err +} + +func (it *zipper) Add(fullpath, relativepath string, details os.FileInfo) { + if details != nil { + common.Debug("- %v size %v", relativepath, details.Size()) + } else { + common.Debug("- %v", relativepath) + } + err := ZipAppend(it.writer, fullpath, relativepath) if err != nil { it.Note(err) } diff --git a/peercc/builder.go b/peercc/builder.go deleted file mode 100644 index 25401268..00000000 --- a/peercc/builder.go +++ /dev/null @@ -1,54 +0,0 @@ -package peercc - -import ( - "path/filepath" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/htfs" - "github.com/robocorp/rcc/pathlib" -) - -func feedInitialSpecs(domain string, specs Specs) { - for _, catalog := range htfs.Catalogs() { - specs <- htfs.NewExportSpec(domain, catalog, []string{}) - } -} - -func buildSpecToStorage(storage string, spec *htfs.ExportSpec) (string, bool) { - holdfile := spec.HoldName() - - defer common.Stopwatch("Build of spec %q -> %q took", spec.Wants, holdfile).Debug() - tree, err := htfs.New() - if err != nil { - return "", false - } - archive := filepath.Join(storage, holdfile) - err = tree.Export([]string{spec.Wants}, spec.Knows, archive) - if err != nil { - return "", false - } - return holdfile, true -} - -func builder(storage string, specs Specs, catalogs Catalogs, holds Holdfiles) { - common.Debug("Builder for %q starting ...", storage) - pathlib.EnsureDirectoryExists(storage) -forever: - for { - todo, ok := <-specs - if !ok { - break forever - } - if todo == nil { - continue - } - common.Debug("Build for %q requested.", todo.HoldName()) - holdfile, ok := buildSpecToStorage(storage, todo) - if ok { - catalogs <- todo.Wants - holds <- holdfile - common.Debug("Build for %q done.", todo.HoldName()) - } - } - common.Debug("Builder stopped!") -} diff --git a/peercc/delta.go b/peercc/delta.go new file mode 100644 index 00000000..4b626d23 --- /dev/null +++ b/peercc/delta.go @@ -0,0 +1,90 @@ +package peercc + +import ( + "archive/zip" + "bufio" + "io" + "net/http" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/set" +) + +func makeDeltaHandler(queries Partqueries) http.HandlerFunc { + return func(response http.ResponseWriter, request *http.Request) { + catalog := filepath.Base(request.URL.Path) + defer common.Stopwatch("Delta of catalog %q took", catalog).Debug() + if request.Method != http.MethodPost { + response.WriteHeader(http.StatusMethodNotAllowed) + common.Trace("Delta: rejecting request %q for catalog %q.", request.Method, catalog) + return + } + reply := make(chan string) + queries <- &Partquery{ + Catalog: catalog, + Reply: reply, + } + content, ok := <-reply + common.Debug("query handler: %q -> %v", catalog, ok) + if !ok { + response.WriteHeader(http.StatusNotFound) + response.Write([]byte("404 not found, sorry")) + return + } + + members := strings.Split(content, "\n") + + requested := make([]string, 0, 1000) + todo := bufio.NewReader(request.Body) + todoloop: + for { + line, err := todo.ReadString('\n') + if err == io.EOF { + break todoloop + } + if err != nil { + common.Debug("DELTA: %v with %q", err, line) + break todoloop + } + flat := strings.TrimSpace(line) + member := set.Member(members, flat) + if !member { + common.Trace("DELTA: ignoring extra %q entry, not part of set!", flat) + continue todoloop + } + requested = append(requested, flat) + } + + headers := response.Header() + headers.Add("Content-Type", "application/zip") + response.WriteHeader(http.StatusOK) + + sink := zip.NewWriter(response) + defer sink.Close() + fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) + relative, err := filepath.Rel(common.HololibLocation(), fullpath) + if err != nil { + common.Debug("DELTA: error %v", err) + return + } + err = operations.ZipAppend(sink, fullpath, relative) + if err != nil { + common.Debug("DELTA: error %v", err) + return + } + + for _, flat := range requested { + relative := htfs.RelativeDefaultLocation(flat) + fullpath := htfs.ExactDefaultLocation(flat) + err = operations.ZipAppend(sink, fullpath, relative) + if err != nil { + common.Debug("DELTA: error %v with %v -> %v", err, fullpath, relative) + return + } + } + } +} diff --git a/peercc/frontdesk.go b/peercc/frontdesk.go deleted file mode 100644 index 51c18790..00000000 --- a/peercc/frontdesk.go +++ /dev/null @@ -1,70 +0,0 @@ -package peercc - -import ( - "fmt" - "net/url" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/htfs" - "github.com/robocorp/rcc/set" -) - -func knownSpec(knowledge []string, spec *htfs.ExportSpec) (*htfs.ExportSpec, *htfs.ExportSpec) { - if !set.Member(knowledge, spec.Wants) { - return nil, nil - } - delta := htfs.NewExportSpec(spec.Domain, spec.Wants, set.Intersect(knowledge, spec.Knows)) - if delta.IsRoot() { - return nil, delta - } - return delta, delta.RootSpec() -} - -func processQuery(available []string, query *Query) *htfs.ExportSpec { - defer close(query.Reply) - - delta, root := knownSpec(available, query.Specification) - if root == nil && delta == nil { - return nil - } - selected := delta - if selected == nil { - selected = root - } - link, err := url.Parse(fmt.Sprintf("/hold/%s", selected.HoldName())) - if err != nil { - return delta - } - query.Reply <- link - return delta -} - -func frontdesk(catalogs Catalogs, holds Holdfiles, queries Queries, specs Specs) { - common.Debug("Frontdesk starting ...") - available := []string{} - holding := []string{} -forever: - for { - select { - case catalog, ok := <-catalogs: - if !ok { - break forever - } - available, _ = set.Update(available, catalog) - case hold, ok := <-holds: - if !ok { - break forever - } - holding, _ = set.Update(holding, hold) - case query, ok := <-queries: - if !ok { - break forever - } - delta := processQuery(available, query) - if delta != nil { - specs <- delta - } - } - } - common.Debug("Frontdesk stopping!") -} diff --git a/peercc/listings.go b/peercc/listings.go index 1aef615e..94cef56f 100644 --- a/peercc/listings.go +++ b/peercc/listings.go @@ -20,6 +20,11 @@ func makeQueryHandler(queries Partqueries) http.HandlerFunc { return func(response http.ResponseWriter, request *http.Request) { catalog := filepath.Base(request.URL.Path) defer common.Stopwatch("Query of catalog %q took", catalog).Debug() + if request.Method != http.MethodGet { + response.WriteHeader(http.StatusMethodNotAllowed) + common.Trace("Query: rejecting request %q for catalog %q.", request.Method, catalog) + return + } reply := make(chan string) queries <- &Partquery{ Catalog: catalog, diff --git a/peercc/messages.go b/peercc/messages.go index 52cb9684..6449fe4c 100644 --- a/peercc/messages.go +++ b/peercc/messages.go @@ -1,21 +1,6 @@ package peercc -import ( - "net/url" - - "github.com/robocorp/rcc/htfs" -) - type ( - URLs chan *url.URL - Query struct { - Specification *htfs.ExportSpec - Reply URLs - } - Queries chan *Query - Catalogs chan string - Holdfiles chan string - Specs chan *htfs.ExportSpec Partquery struct { Catalog string Reply chan string diff --git a/peercc/missing_test.go b/peercc/missing_test.go new file mode 100644 index 00000000..fa32ef9f --- /dev/null +++ b/peercc/missing_test.go @@ -0,0 +1 @@ +package peercc_test diff --git a/peercc/server.go b/peercc/server.go index 11deb8db..23299792 100644 --- a/peercc/server.go +++ b/peercc/server.go @@ -11,20 +11,11 @@ import ( "time" ) -func stopper(server *http.Server) error { - signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) - <-signals - return server.Shutdown(context.TODO()) -} - func Serve(address string, port int, domain, storage string) error { // we need - // - builder - // - holder + // - query handler (for just catalog hashes) + // - partial content sender (for sending delta catalog) // - webserver - // - download handler (for optimists) - // - specification handler (for pessimists) holding := filepath.Join(storage, "hold") err := cleanupHoldStorage(holding) if err != nil { @@ -33,9 +24,6 @@ func Serve(address string, port int, domain, storage string) error { defer cleanupHoldStorage(holding) partqueries := make(Partqueries) - signals := make(chan os.Signal, 1) - - signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) defer close(partqueries) @@ -46,16 +34,22 @@ func Serve(address string, port int, domain, storage string) error { server := &http.Server{ Addr: listen, Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, + ReadTimeout: 20 * time.Second, + WriteTimeout: 40 * time.Second, MaxHeaderBytes: 1 << 14, } mux.HandleFunc("/parts/", makeQueryHandler(partqueries)) + mux.HandleFunc("/delta/", makeDeltaHandler(partqueries)) go server.ListenAndServe() - <-signals + return runTillSignal(server) +} +func runTillSignal(server *http.Server) error { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) + <-signals return server.Shutdown(context.TODO()) } diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index dc91f6a7..dd5ac8cb 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -15,6 +15,9 @@ Prepare Local Create Directory tmp/robocorp Set Environment Variable ROBOCORP_HOME tmp/robocorp + Comment Make sure that tests do not use shared holotree + Fire And Forget build/rcc ht init --revoke + Fire And Forget build/rcc ht delete 4e67cd8 Comment Verify micromamba is installed or download and install it. From c2b14c0e22500c2d4f3e89ea486da6fbb75736bf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 9 Dec 2022 10:07:40 +0200 Subject: [PATCH 333/516] POC: continued on peercc PoC (v11.35.5) --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ peercc/delta.go | 35 ++++++++++++++++++++--------------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/common/version.go b/common/version.go index 18842acf..ea709a71 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.4` + Version = `v11.35.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 703d7520..bd38d4a4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.35.5 (date: 8.12.2022) UNSTABLE + +- fixed bug where last line of request was missing +- trying to fix CodeQL security warning (user input was already filtered based + on known set of values, but analyzer did not understand that) + ## v11.35.4 (date: 8.12.2022) UNSTABLE - removing failed parts of PoC diff --git a/peercc/delta.go b/peercc/delta.go index 4b626d23..11f348d2 100644 --- a/peercc/delta.go +++ b/peercc/delta.go @@ -28,7 +28,7 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { Catalog: catalog, Reply: reply, } - content, ok := <-reply + known, ok := <-reply common.Debug("query handler: %q -> %v", catalog, ok) if !ok { response.WriteHeader(http.StatusNotFound) @@ -36,27 +36,32 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { return } - members := strings.Split(content, "\n") + members := strings.Split(known, "\n") - requested := make([]string, 0, 1000) + approved := make([]string, 0, 1000) todo := bufio.NewReader(request.Body) todoloop: for { line, err := todo.ReadString('\n') - if err == io.EOF { + stopping := err == io.EOF + candidate := filepath.Base(strings.TrimSpace(line)) + if len(candidate) > 0 { + if set.Member(members, candidate) { + approved = append(approved, candidate) + } else { + common.Trace("DELTA: ignoring extra %q entry, not part of set!", candidate) + if !stopping { + continue todoloop + } + } + } + if stopping { break todoloop } if err != nil { - common.Debug("DELTA: %v with %q", err, line) + common.Trace("DELTA: error %v with line %q", err, line) break todoloop } - flat := strings.TrimSpace(line) - member := set.Member(members, flat) - if !member { - common.Trace("DELTA: ignoring extra %q entry, not part of set!", flat) - continue todoloop - } - requested = append(requested, flat) } headers := response.Header() @@ -77,9 +82,9 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { return } - for _, flat := range requested { - relative := htfs.RelativeDefaultLocation(flat) - fullpath := htfs.ExactDefaultLocation(flat) + for _, member := range approved { + relative := htfs.RelativeDefaultLocation(member) + fullpath := htfs.ExactDefaultLocation(member) err = operations.ZipAppend(sink, fullpath, relative) if err != nil { common.Debug("DELTA: error %v with %v -> %v", err, fullpath, relative) From deed3cc60ef975d04a420e496cb0b95502503ae4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 14 Dec 2022 08:44:05 +0200 Subject: [PATCH 334/516] POC: rcc side of functionality (v11.35.6) - bug fix: ignoring dotfiles and directories in "pids" directory - added new `rcc holotree pull` command to do delta environment update request to peercc (still incomplete, does not do automatic import of content) - on delta export zip, catalog will now come as last part of that zip from wire - added set membership map functionality (to make faster membership checks on bigger member sets) - more failed parts of PoC removed (export specification and support functions) --- cmd/holotreeExport.go | 49 ----------------- cmd/holotreePull.go | 46 ++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 12 ++++- htfs/export.go | 64 ----------------------- htfs/pull.go | 107 ++++++++++++++++++++++++++++++++++++++ operations/diagnostics.go | 6 +++ peercc/delta.go | 27 +++++----- set/functions.go | 8 +++ 9 files changed, 193 insertions(+), 128 deletions(-) create mode 100644 cmd/holotreePull.go delete mode 100644 htfs/export.go create mode 100644 htfs/pull.go diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index 3f9c8ac1..e3f99f00 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -2,8 +2,6 @@ package cmd import ( "encoding/json" - "fmt" - "os" "sort" "strings" @@ -16,36 +14,8 @@ import ( var ( holozip string exportRobot string - specFile string ) -func loadExportSpec(filename string) (*htfs.ExportSpec, error) { - raw, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - spec, err := htfs.ParseExportSpec(raw) - if err != nil { - return nil, err - } - return spec, nil -} - -func exportBySpecification(filename string) { - spec, err := loadExportSpec(filename) - pretty.Guard(err == nil, 4, "Loading specification %q failed, reason: %v", filename, err) - known := selectExactCatalogs(spec.Knows) - wants := selectExactCatalogs([]string{spec.Wants}) - pretty.Guard(len(wants) == 1, 5, "Only %d out of 1 needed catalogs available. Quitting!", len(wants)) - unifiedSpec := htfs.NewExportSpec(spec.Domain, spec.Wants, known) - textual, fingerprint, err := unifiedSpec.Fingerprint() - pretty.Guard(err == nil, 6, "Fingerprinting unified specification failed, reason: %v", err) - common.Debug("Final delta specification %0x16x is:\n%s", fingerprint, textual) - deltafile := fmt.Sprintf("%016x.hld", fingerprint) - holotreeExport(wants, unifiedSpec.Knows, deltafile) - common.Stdout("%s\n", deltafile) -} - func holotreeExport(catalogs, known []string, archive string) { common.Debug("Ignoring content from catalogs:") for _, catalog := range known { @@ -77,20 +47,6 @@ func listCatalogs(jsonForm bool) { } } -func selectExactCatalogs(filters []string) []string { - result := make([]string, 0, len(filters)) - for _, catalog := range htfs.Catalogs() { - for _, filter := range filters { - if catalog == filter { - result = append(result, catalog) - break - } - } - } - sort.Strings(result) - return result -} - func selectCatalogs(filters []string) []string { result := make([]string, 0, len(filters)) for _, catalog := range htfs.Catalogs() { @@ -113,10 +69,6 @@ var holotreeExportCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Holotree export command lasted").Report() } - if len(specFile) > 0 { - exportBySpecification(specFile) - return - } if len(exportRobot) > 0 { _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, exportRobot) pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) @@ -134,7 +86,6 @@ var holotreeExportCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeExportCmd) - holotreeExportCmd.Flags().StringVarP(&specFile, "specification", "s", "", "Filename to use as export speficifaction in YAML format.") holotreeExportCmd.Flags().StringVarP(&holozip, "zipfile", "z", "hololib.zip", "Name of zipfile to export.") holotreeExportCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") holotreeExportCmd.Flags().StringVarP(&exportRobot, "robot", "r", "", "Full path to 'robot.yaml' configuration file to export as catalog. ") diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go new file mode 100644 index 00000000..8912e3bf --- /dev/null +++ b/cmd/holotreePull.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + remoteOrigin string + pullRobot string + forcePull bool +) + +var holotreePullCmd = &cobra.Command{ + Use: "pull", + Short: "Try to pull existing holotree catalog from remote source.", + Long: "Try to pull existing holotree catalog from remote source.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree pull command lasted").Report() + } + _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, pullRobot) + pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) + hash := htfs.BlueprintHash(holotreeBlueprint) + tree, err := htfs.New() + pretty.Guard(err == nil, 2, "%s", err) + + present := tree.HasBlueprint(holotreeBlueprint) + if !present || forcePull { + catalog := htfs.CatalogName(hash) + err = htfs.Pull(remoteOrigin, catalog) + pretty.Guard(err == nil, 3, "%s", err) + } + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreePullCmd) + holotreePullCmd.Flags().BoolVarP(&forcePull, "force", "", false, "Force pull check, even when blueprint is already present.") + holotreePullCmd.Flags().StringVarP(&remoteOrigin, "origin", "o", "", "URL of remote origin to pull environment from.") + holotreePullCmd.Flags().StringVarP(&pullRobot, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file to export as catalog. ") + holotreePullCmd.MarkFlagRequired("origin") +} diff --git a/common/version.go b/common/version.go index ea709a71..bc055a65 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.5` + Version = `v11.35.6` ) diff --git a/docs/changelog.md b/docs/changelog.md index bd38d4a4..23e510ff 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,16 @@ # rcc change log -## v11.35.5 (date: 8.12.2022) UNSTABLE +## v11.35.6 (date: 14.12.2022) UNSTABLE + +- bug fix: ignoring dotfiles and directories in "pids" directory +- added new `rcc holotree pull` command to do delta environment update request + to peercc (still incomplete, does not do automatic import of content) +- on delta export zip, catalog will now come as last part of that zip from wire +- added set membership map functionality (to make faster membership checks on + bigger member sets) +- more failed parts of PoC removed (export specification and support functions) + +## v11.35.5 (date: 9.12.2022) UNSTABLE - fixed bug where last line of request was missing - trying to fix CodeQL security warning (user input was already filtered based diff --git a/htfs/export.go b/htfs/export.go deleted file mode 100644 index a13edec3..00000000 --- a/htfs/export.go +++ /dev/null @@ -1,64 +0,0 @@ -package htfs - -import ( - "fmt" - "sort" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/set" - "gopkg.in/yaml.v2" -) - -type ( - ExportSpec struct { - Domain string `yaml:"domain"` - Wants string `yaml:"wants"` - Knows []string `yaml:"knows"` - } -) - -func (it *ExportSpec) IsRoot() bool { - return len(it.Knows) == 0 -} - -func (it *ExportSpec) RootSpec() *ExportSpec { - if it.IsRoot() { - return it - } - return NewExportSpec(it.Domain, it.Wants, []string{}) -} - -func (it *ExportSpec) HoldName() string { - _, fingerprint, err := it.Fingerprint() - if err != nil { - return "broken.hld" - } - return fmt.Sprintf("%016x.hld", fingerprint) -} - -func (it *ExportSpec) Fingerprint() (string, uint64, error) { - sort.Strings(it.Knows) - content, err := yaml.Marshal(it) - if err != nil { - return "", 0, err - } - fingerprint := common.Siphash(9007199254740993, 2147483647, content) - return string(content), fingerprint, nil -} - -func ParseExportSpec(content []byte) (*ExportSpec, error) { - result := &ExportSpec{} - err := yaml.Unmarshal(content, result) - if err != nil { - return nil, err - } - return result, nil -} - -func NewExportSpec(domain, wants string, knows []string) *ExportSpec { - return &ExportSpec{ - Domain: domain, - Wants: wants, - Knows: set.Set(knows), - } -} diff --git a/htfs/pull.go b/htfs/pull.go new file mode 100644 index 00000000..cc9d2019 --- /dev/null +++ b/htfs/pull.go @@ -0,0 +1,107 @@ +package htfs + +import ( + "bufio" + "bytes" + "crypto/sha256" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" + "github.com/robocorp/rcc/xviper" +) + +func pullOriginFingerprints(origin, catalogName string) (fingerprints string, count int, err error) { + defer fail.Around(&err) + + client, err := cloud.NewClient(origin) + fail.On(err != nil, "Could not create web client for %q, reason: %v", origin, err) + + request := client.NewRequest(fmt.Sprintf("/parts/%s", catalogName)) + response := client.Get(request) + pretty.Guard(response.Status == 200, 5, "Problem with parts request, status=%d, body=%s", response.Status, response.Body) + + stream := bufio.NewReader(bytes.NewReader(response.Body)) + collection := make([]string, 0, 2048) + for { + line, err := stream.ReadString('\n') + flat := strings.TrimSpace(line) + if len(flat) > 0 { + fullpath := ExactDefaultLocation(flat) + if !pathlib.IsFile(fullpath) { + collection = append(collection, flat) + } + } + if err == io.EOF { + return strings.Join(collection, "\n"), len(collection), nil + } + fail.On(err != nil, "STREAM error: %v", err) + } + + return "", 0, fmt.Errorf("Unexpected reach of code that should never happen.") +} + +func downloadMissingEnvironmentParts(origin, catalogName, selection string) (filename string, err error) { + defer fail.Around(&err) + + url := fmt.Sprintf("%s/delta/%s", origin, catalogName) + + body := strings.NewReader(selection) + filename = filepath.Join(os.TempDir(), fmt.Sprintf("peercc_%x.zip", os.Getpid())) + + client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} + request, err := http.NewRequest("POST", url, body) + fail.On(err != nil, "Failed create request to %q failed, reason: %v", url, err) + + request.Header.Add("robocorp-installation-id", xviper.TrackingIdentity()) + request.Header.Add("User-Agent", common.UserAgent()) + + response, err := client.Do(request) + fail.On(err != nil, "Web request to %q failed, reason: %v", url, err) + defer response.Body.Close() + + fail.On(response.StatusCode < 200 || 299 < response.StatusCode, "%s (%s)", response.Status, url) + + out, err := os.Create(filename) + fail.On(err != nil, "Creating temporary file %q failed, reason: %v", filename, err) + defer out.Close() + + digest := sha256.New() + many := io.MultiWriter(out, digest) + + common.Debug("Downloading %s <%s> -> %s", url, response.Status, filename) + + _, err = io.Copy(many, response.Body) + fail.On(err != nil, "Download failed, reason: %v", err) + + sum := fmt.Sprintf("%02x", digest.Sum(nil)) + finalname := filepath.Join(os.TempDir(), fmt.Sprintf("peercc_%s.zip", sum)) + err = TryRename("delta", filename, finalname) + fail.On(err != nil, "Rename %q -> %q failed, reason: %v", filename, finalname, err) + + return finalname, nil +} + +func Pull(origin, catalogName string) (err error) { + defer fail.Around(&err) + + unknownSelected, _, err := pullOriginFingerprints(origin, catalogName) + fail.On(err != nil, "%v", err) + + filename, err := downloadMissingEnvironmentParts(origin, catalogName, unknownSelected) + fail.On(err != nil, "%v", err) + + common.Debug("Temporary content based filename is: %q", filename) + common.Debug("FIXME: still missing automatic importing into hololib!") + + return nil +} diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 769aad6b..9edf7f3e 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -212,8 +212,14 @@ func lockpidsCheck() []*common.DiagnosticCheck { } deadline := time.Now().Add(-12 * time.Hour) for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".") { + continue + } level, qualifier := statusWarning, "Pending" info, err := entry.Info() + if info.IsDir() { + continue + } if err == nil && info.ModTime().Before(deadline) { level, qualifier = statusOk, "Stale(?)" } diff --git a/peercc/delta.go b/peercc/delta.go index 11f348d2..c0ed41ac 100644 --- a/peercc/delta.go +++ b/peercc/delta.go @@ -36,7 +36,7 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { return } - members := strings.Split(known, "\n") + membership := set.Membership(strings.Split(known, "\n")) approved := make([]string, 0, 1000) todo := bufio.NewReader(request.Body) @@ -45,8 +45,8 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { line, err := todo.ReadString('\n') stopping := err == io.EOF candidate := filepath.Base(strings.TrimSpace(line)) - if len(candidate) > 0 { - if set.Member(members, candidate) { + if len(candidate) > 10 { + if membership[candidate] { approved = append(approved, candidate) } else { common.Trace("DELTA: ignoring extra %q entry, not part of set!", candidate) @@ -70,6 +70,17 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { sink := zip.NewWriter(response) defer sink.Close() + + for _, member := range approved { + relative := htfs.RelativeDefaultLocation(member) + fullpath := htfs.ExactDefaultLocation(member) + err := operations.ZipAppend(sink, fullpath, relative) + if err != nil { + common.Debug("DELTA: error %v with %v -> %v", err, fullpath, relative) + return + } + } + fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) relative, err := filepath.Rel(common.HololibLocation(), fullpath) if err != nil { @@ -81,15 +92,5 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { common.Debug("DELTA: error %v", err) return } - - for _, member := range approved { - relative := htfs.RelativeDefaultLocation(member) - fullpath := htfs.ExactDefaultLocation(member) - err = operations.ZipAppend(sink, fullpath, relative) - if err != nil { - common.Debug("DELTA: error %v with %v -> %v", err, fullpath, relative) - return - } - } } } diff --git a/set/functions.go b/set/functions.go index c1cd9e7f..c2f4dd79 100644 --- a/set/functions.go +++ b/set/functions.go @@ -48,6 +48,14 @@ func Member[T comparable](set []T, candidate T) bool { return false } +func Membership[T comparable](set []T) map[T]bool { + result := make(map[T]bool) + for _, item := range set { + result[item] = true + } + return result +} + func Update[T comparable](set []T, candidate T) ([]T, bool) { if Member(set, candidate) { return set, false From d444bb59de30dc8a3b910dabd867eced07d89dd7 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Dec 2022 11:21:40 +0200 Subject: [PATCH 335/516] POC: rcc side of functionality (v11.35.7) - this v11.35.x series adds new "peercc" executable and new holotree pull subcommand to rcc; these are work in progress, and not ready for production work yet; do not use, unless you know what you are doing - added automatic import of delta environment update data - tech: moved TryRemove, TryRemoveAll, and TryRename to pathlib - tech: some zipper log verbosity was moved from Debug to Trace level --- cmd/holotreePull.go | 3 +- common/version.go | 2 +- docs/changelog.md | 9 ++++++ htfs/commands.go | 8 ++--- htfs/functions.go | 58 +++--------------------------------- {htfs => operations}/pull.go | 20 +++++++++---- operations/zipper.go | 8 ++--- pathlib/functions.go | 49 ++++++++++++++++++++++++++++++ peercc/manage.go | 3 +- 9 files changed, 88 insertions(+), 72 deletions(-) rename {htfs => operations}/pull.go (81%) diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index 8912e3bf..58f50e0c 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -30,7 +31,7 @@ var holotreePullCmd = &cobra.Command{ present := tree.HasBlueprint(holotreeBlueprint) if !present || forcePull { catalog := htfs.CatalogName(hash) - err = htfs.Pull(remoteOrigin, catalog) + err = operations.PullCatalog(remoteOrigin, catalog) pretty.Guard(err == nil, 3, "%s", err) } pretty.Ok() diff --git a/common/version.go b/common/version.go index bc055a65..7005edb2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.6` + Version = `v11.35.7` ) diff --git a/docs/changelog.md b/docs/changelog.md index 23e510ff..80d27140 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.35.7 (date: 15.12.2022) + +- this v11.35.x series adds new "peercc" executable and new holotree pull + subcommand to rcc; these are work in progress, and not ready for production + work yet; do not use, unless you know what you are doing +- added automatic import of delta environment update data +- tech: moved TryRemove, TryRemoveAll, and TryRename to pathlib +- tech: some zipper log verbosity was moved from Debug to Trace level + ## v11.35.6 (date: 14.12.2022) UNSTABLE - bug fix: ignoring dotfiles and directories in "pids" directory diff --git a/htfs/commands.go b/htfs/commands.go index e506f06f..6d5e3f83 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -104,7 +104,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin func CleanupHolotreeStage(tree MutableLibrary) error { common.Timeline("holotree stage removal start") defer common.Timeline("holotree stage removal done") - return TryRemoveAll("stage", tree.Stage()) + return pathlib.TryRemoveAll("stage", tree.Stage()) } func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorecard common.Scorecard) (err error) { @@ -174,9 +174,9 @@ func RemoveHolotreeSpace(label string) (err error) { if name != label { continue } - TryRemove("metafile", metafile) - TryRemove("lockfile", directory+".lck") - err = TryRemoveAll("space", directory) + pathlib.TryRemove("metafile", metafile) + pathlib.TryRemove("lockfile", directory+".lck") + err = pathlib.TryRemoveAll("space", directory) fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) } return nil diff --git a/htfs/functions.go b/htfs/functions.go index 16e2e14b..b3c17db0 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -5,13 +5,11 @@ import ( "crypto/sha256" "fmt" "io" - "math/rand" "os" "path/filepath" "runtime" "sort" "sync" - "time" "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" @@ -219,54 +217,6 @@ func ScheduleLifters(library MutableLibrary, stats *stats) Treetop { return scheduler } -func TryRemove(context, target string) (err error) { - for delay := 0; delay < 5; delay += 1 { - time.Sleep(time.Duration(delay*100) * time.Millisecond) - err = os.Remove(target) - if err == nil { - return nil - } - } - return fmt.Errorf("Remove failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) -} - -func TryRemoveAll(context, target string) (err error) { - for delay := 0; delay < 5; delay += 1 { - time.Sleep(time.Duration(delay*100) * time.Millisecond) - err = os.RemoveAll(target) - if err == nil { - return nil - } - } - return fmt.Errorf("RemoveAll failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) -} - -func TryRename(context, source, target string) (err error) { - for delay := 0; delay < 5; delay += 1 { - time.Sleep(time.Duration(delay*100) * time.Millisecond) - err = os.Rename(source, target) - if err == nil { - return nil - } - } - common.Debug("Heads up: rename about to fail [%q -> %q], reason: %s", source, target, err) - origin := "source" - intermediate := fmt.Sprintf("%s.%d_%x", source, os.Getpid(), rand.Intn(4096)) - err = os.Rename(source, intermediate) - if err == nil { - source = intermediate - origin = "target" - } - for delay := 0; delay < 5; delay += 1 { - time.Sleep(time.Duration(delay*100) * time.Millisecond) - err = os.Rename(source, target) - if err == nil { - return nil - } - } - return fmt.Errorf("Rename failure [%s, %s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, origin, err) -} - func LiftFile(sourcename, sinkname string) anywork.Work { return func() { source, err := os.Open(sourcename) @@ -291,7 +241,7 @@ func LiftFile(sourcename, sinkname string) anywork.Work { runtime.Gosched() - anywork.OnErrPanicCloseAll(TryRename("liftfile", partname, sinkname)) + anywork.OnErrPanicCloseAll(pathlib.TryRename("liftfile", partname, sinkname)) pathlib.MakeSharedFile(sinkname) } } @@ -335,7 +285,7 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ anywork.OnErrPanicCloseAll(sink.Close()) - anywork.OnErrPanicCloseAll(TryRename("dropfile", partname, sinkname)) + anywork.OnErrPanicCloseAll(pathlib.TryRename("dropfile", partname, sinkname)) anywork.OnErrPanicCloseAll(os.Chmod(sinkname, details.Mode)) anywork.OnErrPanicCloseAll(os.Chtimes(sinkname, motherTime, motherTime)) @@ -344,13 +294,13 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ func RemoveFile(filename string) anywork.Work { return func() { - anywork.OnErrPanicCloseAll(TryRemove("file", filename)) + anywork.OnErrPanicCloseAll(pathlib.TryRemove("file", filename)) } } func RemoveDirectory(dirname string) anywork.Work { return func() { - anywork.OnErrPanicCloseAll(TryRemoveAll("directory", dirname)) + anywork.OnErrPanicCloseAll(pathlib.TryRemoveAll("directory", dirname)) } } diff --git a/htfs/pull.go b/operations/pull.go similarity index 81% rename from htfs/pull.go rename to operations/pull.go index cc9d2019..fee8a1e2 100644 --- a/htfs/pull.go +++ b/operations/pull.go @@ -1,4 +1,4 @@ -package htfs +package operations import ( "bufio" @@ -14,6 +14,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" @@ -36,7 +37,7 @@ func pullOriginFingerprints(origin, catalogName string) (fingerprints string, co line, err := stream.ReadString('\n') flat := strings.TrimSpace(line) if len(flat) > 0 { - fullpath := ExactDefaultLocation(flat) + fullpath := htfs.ExactDefaultLocation(flat) if !pathlib.IsFile(fullpath) { collection = append(collection, flat) } @@ -74,6 +75,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil out, err := os.Create(filename) fail.On(err != nil, "Creating temporary file %q failed, reason: %v", filename, err) defer out.Close() + defer pathlib.TryRemove("temporary", filename) digest := sha256.New() many := io.MultiWriter(out, digest) @@ -85,23 +87,29 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil sum := fmt.Sprintf("%02x", digest.Sum(nil)) finalname := filepath.Join(os.TempDir(), fmt.Sprintf("peercc_%s.zip", sum)) - err = TryRename("delta", filename, finalname) + err = pathlib.TryRename("delta", filename, finalname) fail.On(err != nil, "Rename %q -> %q failed, reason: %v", filename, finalname, err) return finalname, nil } -func Pull(origin, catalogName string) (err error) { +func PullCatalog(origin, catalogName string) (err error) { defer fail.Around(&err) - unknownSelected, _, err := pullOriginFingerprints(origin, catalogName) + common.Timeline("pull %q parts from %q", catalogName, origin) + unknownSelected, count, err := pullOriginFingerprints(origin, catalogName) fail.On(err != nil, "%v", err) + common.Timeline("download %d parts + catalog from %q", count, origin) filename, err := downloadMissingEnvironmentParts(origin, catalogName, unknownSelected) fail.On(err != nil, "%v", err) common.Debug("Temporary content based filename is: %q", filename) - common.Debug("FIXME: still missing automatic importing into hololib!") + defer pathlib.TryRemove("temporary", filename) + + err = Unzip(common.HololibLocation(), filename, true, false) + fail.On(err != nil, "Failed to unzip %v to hololib, reason: %v", filename, err) + common.Timeline("environment pull completed") return nil } diff --git a/operations/zipper.go b/operations/zipper.go index d06c967a..e2ca35b5 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -50,10 +50,10 @@ func (it *WriteTarget) Execute() bool { return false } defer target.Close() - common.Debug("- %v", it.Target) + common.Trace("- %v", it.Target) _, err = io.Copy(target, source) if err != nil { - common.Debug(" - failure: %v", err) + common.Debug(" - failure with %q, reason: %v", it.Target, err) } os.Chtimes(it.Target, it.Source.Modified, it.Source.Modified) return err == nil @@ -160,7 +160,7 @@ func (it *unzipper) Asset(name string) ([]byte, error) { } func (it *unzipper) Extract(directory string) error { - common.Debug("Extracting:") + common.Trace("Extracting:") success := true for _, entry := range it.reader.File { if entry.FileInfo().IsDir() { @@ -173,7 +173,7 @@ func (it *unzipper) Extract(directory string) error { } success = todo.Execute() && success } - common.Debug("Done.") + common.Trace("Done.") if !success { return fmt.Errorf("Problems while unwrapping robot. Use --debug to see details.") } diff --git a/pathlib/functions.go b/pathlib/functions.go index 9f0745bb..056a072c 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -3,6 +3,7 @@ package pathlib import ( "fmt" "io/fs" + "math/rand" "os" "path/filepath" "time" @@ -85,6 +86,54 @@ func Modtime(pathname string) (time.Time, error) { return stat.ModTime(), nil } +func TryRemove(context, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Remove(target) + if err == nil { + return nil + } + } + return fmt.Errorf("Remove failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) +} + +func TryRemoveAll(context, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.RemoveAll(target) + if err == nil { + return nil + } + } + return fmt.Errorf("RemoveAll failure [%s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, err) +} + +func TryRename(context, source, target string) (err error) { + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Rename(source, target) + if err == nil { + return nil + } + } + common.Debug("Heads up: rename about to fail [%q -> %q], reason: %s", source, target, err) + origin := "source" + intermediate := fmt.Sprintf("%s.%d_%x", source, os.Getpid(), rand.Intn(4096)) + err = os.Rename(source, intermediate) + if err == nil { + source = intermediate + origin = "target" + } + for delay := 0; delay < 5; delay += 1 { + time.Sleep(time.Duration(delay*100) * time.Millisecond) + err = os.Rename(source, target) + if err == nil { + return nil + } + } + return fmt.Errorf("Rename failure [%s, %s, %s, %s], reason: %s", context, common.ControllerIdentity(), common.HolotreeSpace, origin, err) +} + func hasCorrectMode(stat fs.FileInfo, expected fs.FileMode) bool { return expected == (stat.Mode() & expected) } diff --git a/peercc/manage.go b/peercc/manage.go index 61b491cc..5963d656 100644 --- a/peercc/manage.go +++ b/peercc/manage.go @@ -4,7 +4,6 @@ import ( "path/filepath" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" ) @@ -17,7 +16,7 @@ func cleanupHoldStorage(storage string) error { return err } for _, filename := range filenames { - err = htfs.TryRemove("hold", filename) + err = pathlib.TryRemove("hold", filename) if err != nil { return err } From d01a98993897158df9dddd282cd20c8086c207a5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Dec 2022 13:17:42 +0200 Subject: [PATCH 336/516] Diagnostics JSON output has new category (v11.36.0) - added category field into diagnostics JSON output, to support applications to report better diagnostics messages --- common/categories.go | 15 +++ common/diagnostics.go | 41 ++++--- common/version.go | 2 +- conda/condayaml.go | 26 ++--- docs/changelog.md | 5 + operations/diagnostics.go | 224 +++++++++++++++++++++----------------- robot/robot.go | 44 ++++---- settings/data.go | 18 +-- settings/settings.go | 4 +- 9 files changed, 215 insertions(+), 164 deletions(-) create mode 100644 common/categories.go diff --git a/common/categories.go b/common/categories.go new file mode 100644 index 00000000..dfd81e23 --- /dev/null +++ b/common/categories.go @@ -0,0 +1,15 @@ +package common + +const ( + CategoryUndefined = 0 + CategoryLongPath = 1010 + CategoryLockFile = 1020 + CategoryLockPid = 1021 + CategoryPathCheck = 1030 + CategoryHolotreeShared = 2010 + CategoryRobocorpHome = 3010 + CategoryNetworkDNS = 4010 + CategoryNetworkLink = 4020 + CategoryNetworkHEAD = 4030 + CategoryNetworkCanary = 4040 +) diff --git a/common/diagnostics.go b/common/diagnostics.go index 1e88830d..40e3a412 100644 --- a/common/diagnostics.go +++ b/common/diagnostics.go @@ -12,22 +12,22 @@ const ( StatusFatal = `fatal` ) -type Diagnoser func(status, link, form string, details ...interface{}) +type Diagnoser func(category uint64, status, link, form string, details ...interface{}) -func (it Diagnoser) Ok(form string, details ...interface{}) { - it(StatusOk, "", form, details...) +func (it Diagnoser) Ok(category uint64, form string, details ...interface{}) { + it(category, StatusOk, "", form, details...) } -func (it Diagnoser) Warning(link, form string, details ...interface{}) { - it(StatusWarning, link, form, details...) +func (it Diagnoser) Warning(category uint64, link, form string, details ...interface{}) { + it(category, StatusWarning, link, form, details...) } -func (it Diagnoser) Fail(link, form string, details ...interface{}) { - it(StatusFail, link, form, details...) +func (it Diagnoser) Fail(category uint64, link, form string, details ...interface{}) { + it(category, StatusFail, link, form, details...) } -func (it Diagnoser) Fatal(link, form string, details ...interface{}) { - it(StatusFatal, link, form, details...) +func (it Diagnoser) Fatal(category uint64, link, form string, details ...interface{}) { + it(category, StatusFatal, link, form, details...) } type DiagnosticStatus struct { @@ -36,19 +36,26 @@ type DiagnosticStatus struct { } type DiagnosticCheck struct { - Type string `json:"type"` - Status string `json:"status"` - Message string `json:"message"` - Link string `json:"url"` + Type string `json:"type"` + Category uint64 `json:"category"` + Status string `json:"status"` + Message string `json:"message"` + Link string `json:"url"` } -func (it *DiagnosticStatus) check(kind, status, message, link string) { - it.Checks = append(it.Checks, &DiagnosticCheck{kind, status, message, link}) +func (it *DiagnosticStatus) check(category uint64, kind, status, message, link string) { + it.Checks = append(it.Checks, &DiagnosticCheck{ + Type: kind, + Category: category, + Status: status, + Message: message, + Link: link, + }) } func (it *DiagnosticStatus) Diagnose(kind string) Diagnoser { - return func(status, link, form string, details ...interface{}) { - it.check(kind, status, fmt.Sprintf(form, details...), link) + return func(category uint64, status, link, form string, details ...interface{}) { + it.check(category, kind, status, fmt.Sprintf(form, details...), link) } } diff --git a/common/version.go b/common/version.go index 7005edb2..8b45fe27 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.35.7` + Version = `v11.36.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 3631de20..b8d2a0b7 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -478,65 +478,65 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b for index, channel := range it.Channels { if channel == "defaults" { defaultsPostion = index - diagnose.Warning("", "Try to avoid defaults channel, and prefer using conda-forge instead.") + diagnose.Warning(0, "", "Try to avoid defaults channel, and prefer using conda-forge instead.") ok = false } } if defaultsPostion == 0 && countChannels > 1 { - diagnose.Warning("", "Try to avoid putting defaults channel as first channel.") + diagnose.Warning(0, "", "Try to avoid putting defaults channel as first channel.") ok = false } if countChannels > 1 { - diagnose.Warning("", "Try to avoid multiple channel. They may cause problems with code compatibility.") + diagnose.Warning(0, "", "Try to avoid multiple channel. They may cause problems with code compatibility.") ok = false } if ok { - diagnose.Ok("Channels in conda.yaml are ok.") + diagnose.Ok(0, "Channels in conda.yaml are ok.") } ok = true for _, dependency := range it.Conda { presentation := dependency.Representation() if packages[presentation] { - notice("", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) + notice(0, "", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) } packages[presentation] = true if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { - notice("", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + notice(0, "", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false floating = true } if len(dependency.Qualifier) > 0 && !(dependency.Qualifier == "==" || dependency.Qualifier == "=") { - diagnose.Fail("", "Conda dependency %q must use '==' or '=' for version declaration.", dependency.Original) + diagnose.Fail(0, "", "Conda dependency %q must use '==' or '=' for version declaration.", dependency.Original) ok = false floating = true } } if ok { - diagnose.Ok("Conda dependencies in conda.yaml are ok.") + diagnose.Ok(0, "Conda dependencies in conda.yaml are ok.") } ok = true for _, dependency := range it.Pip { presentation := dependency.Representation() if packages[presentation] { - notice("", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) + notice(0, "", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) } packages[presentation] = true if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { - notice("", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) + notice(0, "", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false floating = true } if len(dependency.Qualifier) > 0 && dependency.Qualifier != "==" { - diagnose.Fail("", "Pip dependency %q must use '==' for version declaration.", dependency.Original) + diagnose.Fail(0, "", "Pip dependency %q must use '==' for version declaration.", dependency.Original) ok = false floating = true } } if ok { - diagnose.Ok("Pip dependencies in conda.yaml are ok.") + diagnose.Ok(0, "Pip dependencies in conda.yaml are ok.") } if floating { - diagnose.Warning("", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") + diagnose.Warning(0, "", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") } } diff --git a/docs/changelog.md b/docs/changelog.md index 80d27140..5783d53e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.36.0 (date: 15.12.2022) + +- added category field into diagnostics JSON output, to support applications + to report better diagnostics messages + ## v11.35.7 (date: 15.12.2022) - this v11.35.x series adds new "peercc" executable and new holotree pull diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 9edf7f3e..4d07f62b 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -154,17 +154,19 @@ func longPathSupportCheck() *common.DiagnosticCheck { supportLongPathUrl := settings.Global.DocsLink("troubleshooting/windows-long-path") if conda.HasLongPathSupport() { return &common.DiagnosticCheck{ - Type: "OS", - Status: statusOk, - Message: "Supports long enough paths.", - Link: supportLongPathUrl, + Type: "OS", + Category: common.CategoryLongPath, + Status: statusOk, + Message: "Supports long enough paths.", + Link: supportLongPathUrl, } } return &common.DiagnosticCheck{ - Type: "OS", - Status: statusFail, - Message: "Does not support long path names!", - Link: supportLongPathUrl, + Type: "OS", + Category: common.CategoryLongPath, + Status: statusFail, + Message: "Does not support long path names!", + Link: supportLongPathUrl, } } @@ -178,20 +180,22 @@ func lockfilesCheck() []*common.DiagnosticCheck { err := os.WriteFile(filename, content, 0o666) if err != nil { result = append(result, &common.DiagnosticCheck{ - Type: "OS", - Status: statusFail, - Message: fmt.Sprintf("Lock file %q write failed, reason: %v", identity, err), - Link: support, + Type: "OS", + Category: common.CategoryLockFile, + Status: statusFail, + Message: fmt.Sprintf("Lock file %q write failed, reason: %v", identity, err), + Link: support, }) failed = true } } if !failed { result = append(result, &common.DiagnosticCheck{ - Type: "OS", - Status: statusOk, - Message: fmt.Sprintf("%d lockfiles all seem to work correctly (for this user).", len(files)), - Link: support, + Type: "OS", + Category: common.CategoryLockFile, + Status: statusOk, + Message: fmt.Sprintf("%d lockfiles all seem to work correctly (for this user).", len(files)), + Link: support, }) } return result @@ -203,10 +207,11 @@ func lockpidsCheck() []*common.DiagnosticCheck { entries, err := os.ReadDir(common.HololibPids()) if err != nil { result = append(result, &common.DiagnosticCheck{ - Type: "OS", - Status: statusWarning, - Message: fmt.Sprintf("Problem with pids directory: %q, reason: %v", common.HololibPids(), err), - Link: support, + Type: "OS", + Category: common.CategoryLockPid, + Status: statusWarning, + Message: fmt.Sprintf("Problem with pids directory: %q, reason: %v", common.HololibPids(), err), + Link: support, }) return result } @@ -224,18 +229,20 @@ func lockpidsCheck() []*common.DiagnosticCheck { level, qualifier = statusOk, "Stale(?)" } result = append(result, &common.DiagnosticCheck{ - Type: "OS", - Status: level, - Message: fmt.Sprintf("%s lock file info: %q", qualifier, entry.Name()), - Link: support, + Type: "OS", + Category: common.CategoryLockPid, + Status: level, + Message: fmt.Sprintf("%s lock file info: %q", qualifier, entry.Name()), + Link: support, }) } if len(result) == 0 { result = append(result, &common.DiagnosticCheck{ - Type: "OS", - Status: statusOk, - Message: "No pending lock files detected.", - Link: support, + Type: "OS", + Category: common.CategoryLockPid, + Status: statusOk, + Message: "No pending lock files detected.", + Link: support, }) } return result @@ -246,17 +253,19 @@ func anyPathCheck(key string) *common.DiagnosticCheck { anyPath := os.Getenv(key) if len(anyPath) > 0 { return &common.DiagnosticCheck{ - Type: "OS", - Status: statusWarning, - Message: fmt.Sprintf("%s is set to %q. This may cause problems.", key, anyPath), - Link: supportGeneralUrl, + Type: "OS", + Category: common.CategoryPathCheck, + Status: statusWarning, + Message: fmt.Sprintf("%s is set to %q. This may cause problems.", key, anyPath), + Link: supportGeneralUrl, } } return &common.DiagnosticCheck{ - Type: "OS", - Status: statusOk, - Message: fmt.Sprintf("%s is not set, which is good.", key), - Link: supportGeneralUrl, + Type: "OS", + Category: common.CategoryPathCheck, + Status: statusOk, + Message: fmt.Sprintf("%s is not set, which is good.", key), + Link: supportGeneralUrl, } } @@ -265,17 +274,19 @@ func verifySharedDirectory(fullpath string) *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !shared { return &common.DiagnosticCheck{ - Type: "OS", - Status: statusWarning, - Message: fmt.Sprintf("%q is not shared. This may cause problems.", fullpath), - Link: supportGeneralUrl, + Type: "OS", + Category: common.CategoryHolotreeShared, + Status: statusWarning, + Message: fmt.Sprintf("%q is not shared. This may cause problems.", fullpath), + Link: supportGeneralUrl, } } return &common.DiagnosticCheck{ - Type: "OS", - Status: statusOk, - Message: fmt.Sprintf("%q is shared, which is ok.", fullpath), - Link: supportGeneralUrl, + Type: "OS", + Category: common.CategoryHolotreeShared, + Status: statusOk, + Message: fmt.Sprintf("%q is shared, which is ok.", fullpath), + Link: supportGeneralUrl, } } @@ -283,17 +294,19 @@ func robocorpHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { return &common.DiagnosticCheck{ - Type: "RPA", - Status: statusFatal, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", common.RobocorpHome()), - Link: supportGeneralUrl, + Type: "RPA", + Category: common.CategoryRobocorpHome, + Status: statusFatal, + Message: fmt.Sprintf("ROBOCORP_HOME (%s) contains characters that makes RPA fail.", common.RobocorpHome()), + Link: supportGeneralUrl, } } return &common.DiagnosticCheck{ - Type: "RPA", - Status: statusOk, - Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", common.RobocorpHome()), - Link: supportGeneralUrl, + Type: "RPA", + Category: common.CategoryRobocorpHome, + Status: statusOk, + Message: fmt.Sprintf("ROBOCORP_HOME (%s) is good enough.", common.RobocorpHome()), + Link: supportGeneralUrl, } } @@ -302,17 +315,19 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { found, err := net.LookupHost(site) if err != nil { return &common.DiagnosticCheck{ - Type: "network", - Status: statusFail, - Message: fmt.Sprintf("DNS lookup %q failed: %v", site, err), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkDNS, + Status: statusFail, + Message: fmt.Sprintf("DNS lookup %q failed: %v", site, err), + Link: supportNetworkUrl, } } return &common.DiagnosticCheck{ - Type: "network", - Status: statusOk, - Message: fmt.Sprintf("%s found: %v", site, found), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkDNS, + Status: statusOk, + Message: fmt.Sprintf("%s found: %v", site, found), + Link: supportNetworkUrl, } } @@ -321,27 +336,30 @@ func condaHeadCheck() *common.DiagnosticCheck { client, err := cloud.NewClient(settings.Global.CondaLink("")) if err != nil { return &common.DiagnosticCheck{ - Type: "network", - Status: statusWarning, - Message: fmt.Sprintf("%v: %v", settings.Global.CondaLink(""), err), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkLink, + Status: statusWarning, + Message: fmt.Sprintf("%v: %v", settings.Global.CondaLink(""), err), + Link: supportNetworkUrl, } } request := client.NewRequest(condaCanaryUrl) response := client.Head(request) if response.Status >= 400 { return &common.DiagnosticCheck{ - Type: "network", - Status: statusWarning, - Message: fmt.Sprintf("Conda canary download failed: %d", response.Status), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkHEAD, + Status: statusWarning, + Message: fmt.Sprintf("Conda canary download failed: %d", response.Status), + Link: supportNetworkUrl, } } return &common.DiagnosticCheck{ - Type: "network", - Status: statusOk, - Message: fmt.Sprintf("Conda canary download successful: %s", settings.Global.CondaLink(condaCanaryUrl)), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkHEAD, + Status: statusOk, + Message: fmt.Sprintf("Conda canary download successful: %s", settings.Global.CondaLink(condaCanaryUrl)), + Link: supportNetworkUrl, } } @@ -350,27 +368,30 @@ func pypiHeadCheck() *common.DiagnosticCheck { client, err := cloud.NewClient(settings.Global.PypiLink("")) if err != nil { return &common.DiagnosticCheck{ - Type: "network", - Status: statusWarning, - Message: fmt.Sprintf("%v: %v", settings.Global.PypiLink(""), err), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkLink, + Status: statusWarning, + Message: fmt.Sprintf("%v: %v", settings.Global.PypiLink(""), err), + Link: supportNetworkUrl, } } request := client.NewRequest(pypiCanaryUrl) response := client.Head(request) if response.Status >= 400 { return &common.DiagnosticCheck{ - Type: "network", - Status: statusWarning, - Message: fmt.Sprintf("PyPI canary download failed: %d", response.Status), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkHEAD, + Status: statusWarning, + Message: fmt.Sprintf("PyPI canary download failed: %d", response.Status), + Link: supportNetworkUrl, } } return &common.DiagnosticCheck{ - Type: "network", - Status: statusOk, - Message: fmt.Sprintf("PyPI canary download successful: %s", settings.Global.PypiLink(pypiCanaryUrl)), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkHEAD, + Status: statusOk, + Message: fmt.Sprintf("PyPI canary download successful: %s", settings.Global.PypiLink(pypiCanaryUrl)), + Link: supportNetworkUrl, } } @@ -379,27 +400,30 @@ func canaryDownloadCheck() *common.DiagnosticCheck { client, err := cloud.NewClient(settings.Global.DownloadsLink("")) if err != nil { return &common.DiagnosticCheck{ - Type: "network", - Status: statusFail, - Message: fmt.Sprintf("%v: %v", settings.Global.DownloadsLink(""), err), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkLink, + Status: statusFail, + Message: fmt.Sprintf("%v: %v", settings.Global.DownloadsLink(""), err), + Link: supportNetworkUrl, } } request := client.NewRequest(canaryUrl) response := client.Get(request) if response.Status != 200 || string(response.Body) != "Used to testing connections" { return &common.DiagnosticCheck{ - Type: "network", - Status: statusFail, - Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkCanary, + Status: statusFail, + Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), + Link: supportNetworkUrl, } } return &common.DiagnosticCheck{ - Type: "network", - Status: statusOk, - Message: fmt.Sprintf("Canary download successful: %s", settings.Global.DownloadsLink(canaryUrl)), - Link: supportNetworkUrl, + Type: "network", + Category: common.CategoryNetworkCanary, + Status: statusOk, + Message: fmt.Sprintf("Canary download successful: %s", settings.Global.DownloadsLink(canaryUrl)), + Link: supportNetworkUrl, } } @@ -483,18 +507,18 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str } content, err := os.ReadFile(fullpath) if err != nil { - diagnose.Fail(supportGeneralUrl, "Problem reading %s file %q: %v", label, tail, err) + diagnose.Fail(0, supportGeneralUrl, "Problem reading %s file %q: %v", label, tail, err) success = false continue } err = tool(content, &canary) if err != nil { - diagnose.Fail(supportGeneralUrl, "Problem parsing %s file %q: %v", label, tail, err) + diagnose.Fail(0, supportGeneralUrl, "Problem parsing %s file %q: %v", label, tail, err) success = false } } if investigated && success { - diagnose.Ok("%s files are readable and can be parsed.", label) + diagnose.Ok(0, "%s files are readable and can be parsed.", label) } } @@ -511,7 +535,7 @@ func addRobotDiagnostics(robotfile string, target *common.DiagnosticStatus, prod config, err := robot.LoadRobotYaml(robotfile, false) diagnose := target.Diagnose("Robot") if err != nil { - diagnose.Fail(supportGeneralUrl, "About robot.yaml: %v", err) + diagnose.Fail(0, supportGeneralUrl, "About robot.yaml: %v", err) } else { config.Diagnostics(target, production) } diff --git a/robot/robot.go b/robot/robot.go index 40653d4e..e9fdfb53 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -98,15 +98,15 @@ func (it *robot) relink() { func (it *robot) diagnoseTasks(diagnose common.Diagnoser) { if it.Tasks == nil { - diagnose.Fail("", "Missing 'tasks:' from robot.yaml.") + diagnose.Fail(0, "", "Missing 'tasks:' from robot.yaml.") return } ok := true if len(it.Tasks) == 0 { - diagnose.Fail("", "There must be at least one task defined in 'tasks:' section in robot.yaml.") + diagnose.Fail(0, "", "There must be at least one task defined in 'tasks:' section in robot.yaml.") ok = false } else { - diagnose.Ok("Tasks are defined in robot.yaml") + diagnose.Ok(0, "Tasks are defined in robot.yaml") } for name, task := range it.Tasks { count := 0 @@ -120,12 +120,12 @@ func (it *robot) diagnoseTasks(diagnose common.Diagnoser) { count += 1 } if count != 1 { - diagnose.Fail("", "In robot.yaml, task '%s' needs exactly one of robotTaskName/shell/command definition!", name) + diagnose.Fail(0, "", "In robot.yaml, task '%s' needs exactly one of robotTaskName/shell/command definition!", name) ok = false } } if ok { - diagnose.Ok("Each task has exactly one definition.") + diagnose.Ok(0, "Each task has exactly one definition.") } } @@ -133,48 +133,48 @@ func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { ok := true for _, path := range it.Path { if filepath.IsAbs(path) { - diagnose.Fail("", "PATH entry %q seems to be absolute, which makes robot machine dependent.", path) + diagnose.Fail(0, "", "PATH entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } } if ok { - diagnose.Ok("PATH settings in robot.yaml are ok.") + diagnose.Ok(0, "PATH settings in robot.yaml are ok.") } ok = true for _, path := range it.Pythonpath { if filepath.IsAbs(path) { - diagnose.Fail("", "PYTHONPATH entry %q seems to be absolute, which makes robot machine dependent.", path) + diagnose.Fail(0, "", "PYTHONPATH entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } } if ok { - diagnose.Ok("PYTHONPATH settings in robot.yaml are ok.") + diagnose.Ok(0, "PYTHONPATH settings in robot.yaml are ok.") } ok = true if it.Ignored == nil || len(it.Ignored) == 0 { - diagnose.Warning("", "No ignoreFiles defined, so everything ends up inside robot.zip file.") + diagnose.Warning(0, "", "No ignoreFiles defined, so everything ends up inside robot.zip file.") ok = false } else { for at, path := range it.Ignored { if len(strings.TrimSpace(path)) == 0 { - diagnose.Fail("", "there is empty entry in ignoreFiles at position %d", at+1) + diagnose.Fail(0, "", "there is empty entry in ignoreFiles at position %d", at+1) ok = false continue } if filepath.IsAbs(path) { - diagnose.Fail("", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) + diagnose.Fail(0, "", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } } for _, path := range it.IgnoreFiles() { if !pathlib.IsFile(path) { - diagnose.Fail("", "ignoreFiles entry %q is not a file.", path) + diagnose.Fail(0, "", "ignoreFiles entry %q is not a file.", path) ok = false } } } if ok { - diagnose.Ok("ignoreFiles settings in robot.yaml are ok.") + diagnose.Ok(0, "ignoreFiles settings in robot.yaml are ok.") } } @@ -183,24 +183,24 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { it.diagnoseTasks(diagnose) it.diagnoseVariousPaths(diagnose) if it.Artifacts == "" { - diagnose.Fail("", "In robot.yaml, 'artifactsDir:' is required!") + diagnose.Fail(0, "", "In robot.yaml, 'artifactsDir:' is required!") } else { if filepath.IsAbs(it.Artifacts) { - diagnose.Fail("", "artifactDir %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) + diagnose.Fail(0, "", "artifactDir %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) } else { - diagnose.Ok("Artifacts directory defined in robot.yaml") + diagnose.Ok(0, "Artifacts directory defined in robot.yaml") } } if it.Conda == "" { - diagnose.Ok("In robot.yaml, 'condaConfigFile:' is missing. So this is shell robot.") + diagnose.Ok(0, "In robot.yaml, 'condaConfigFile:' is missing. So this is shell robot.") } else { if filepath.IsAbs(it.Conda) { - diagnose.Fail("", "condaConfigFile %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) + diagnose.Fail(0, "", "condaConfigFile %q seems to be absolute, which makes robot machine dependent.", it.Artifacts) } else { - diagnose.Ok("In robot.yaml, 'condaConfigFile:' is present. So this is python robot.") + diagnose.Ok(0, "In robot.yaml, 'condaConfigFile:' is present. So this is python robot.") condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) if err != nil { - diagnose.Fail("", "From robot.yaml, loading conda.yaml failed with: %v", err) + diagnose.Fail(0, "", "From robot.yaml, loading conda.yaml failed with: %v", err) } else { condaEnv.Diagnostics(target, production) } @@ -219,7 +219,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { dependencies = "missing" } else { if it.VerifyCondaDependencies() { - diagnose.Ok("Dependencies in conda.yaml and dependencies.yaml match.") + diagnose.Ok(0, "Dependencies in conda.yaml and dependencies.yaml match.") } } target.Details["robot-dependencies-yaml"] = dependencies diff --git a/settings/data.go b/settings/data.go index 03a3a19c..6857d3e7 100644 --- a/settings/data.go +++ b/settings/data.go @@ -160,16 +160,16 @@ func (it *Settings) AsJson() ([]byte, error) { func diagnoseUrl(link, label string, diagnose common.Diagnoser, correct bool) bool { if len(link) == 0 { - diagnose.Fatal("", "required %q URL is missing.", label) + diagnose.Fatal(0, "", "required %q URL is missing.", label) return false } if !strings.HasPrefix(link, httpsPrefix) { - diagnose.Fatal("", "%q URL %q is does not start with %q prefix.", label, link, httpsPrefix) + diagnose.Fatal(0, "", "%q URL %q is does not start with %q prefix.", label, link, httpsPrefix) return false } _, err := url.Parse(link) if err != nil { - diagnose.Fatal("", "%q URL %q cannot be parsed, reason %v.", label, link, err) + diagnose.Fatal(0, "", "%q URL %q cannot be parsed, reason %v.", label, link, err) return false } return correct @@ -187,14 +187,14 @@ func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStat diagnose := target.Diagnose("settings.yaml") correct := true if it.Endpoints == nil { - diagnose.Fatal("", "endpoints section is totally missing") + diagnose.Fatal(0, "", "endpoints section is totally missing") correct = false } else { correct = diagnoseUrl(it.Endpoints["cloud-api"], "endpoints/cloud-api", diagnose, correct) correct = diagnoseUrl(it.Endpoints["downloads"], "endpoints/downloads", diagnose, correct) } if correct { - diagnose.Ok("Toplevel settings are ok.") + diagnose.Ok(0, "Toplevel settings are ok.") } } @@ -202,11 +202,11 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { diagnose := target.Diagnose("Settings") correct := true if it.Certificates == nil { - diagnose.Warning("", "settings.yaml: certificates section is totally missing") + diagnose.Warning(0, "", "settings.yaml: certificates section is totally missing") correct = false } if it.Endpoints == nil { - diagnose.Warning("", "settings.yaml: endpoints section is totally missing") + diagnose.Warning(0, "", "settings.yaml: endpoints section is totally missing") correct = false } else { correct = diagnoseUrl(it.Endpoints["cloud-api"], "endpoints/cloud-api", diagnose, correct) @@ -222,11 +222,11 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { correct = diagnoseOptionalUrl(it.Endpoints["pypi-trusted"], "endpoints/pypi-trusted", diagnose, correct) } if it.Meta == nil { - diagnose.Warning("", "settings.yaml: meta section is totally missing") + diagnose.Warning(0, "", "settings.yaml: meta section is totally missing") correct = false } if correct { - diagnose.Ok("Toplevel settings are ok.") + diagnose.Ok(0, "Toplevel settings are ok.") } } diff --git a/settings/settings.go b/settings/settings.go index 48937aa8..f9edd7b9 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -105,9 +105,9 @@ func CriticalEnvironmentSettingsCheck() { config.CriticalEnvironmentDiagnostics(result) diagnose := result.Diagnose("Settings") if HasCustomSettings() { - diagnose.Ok("Uses custom settings at %q.", common.SettingsFile()) + diagnose.Ok(0, "Uses custom settings at %q.", common.SettingsFile()) } else { - diagnose.Ok("Uses builtin settings.") + diagnose.Ok(0, "Uses builtin settings.") } fatal, fail, _, _ := result.Counts() if (fatal + fail) > 0 { From f2bda9aaa23092df868dde86d6dcf7cc34f9e14d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 20 Dec 2022 16:10:12 +0200 Subject: [PATCH 337/516] BUGFIX: lock pid file issues (v11.36.1) - bugfix: diagnostics fail on new machine to touch lock files when directory does not exist, this closes #43 - bugfix: stale lock pid files are shown too often, this closes #42 - diagnostics will now show hopefully more human friendly message when active locks are detected - added more runtime.Gosched calls to enable background go routines to have chance to finish before application closes --- anywork/worker.go | 8 +- cloud/metrics.go | 1 + cmd/rcc/main.go | 4 + common/logger.go | 2 + common/version.go | 2 +- docs/changelog.md | 10 ++ operations/diagnostics.go | 30 +++--- pathlib/lock.go | 28 +----- pathlib/lock_unix.go | 16 +--- pathlib/lock_windows.go | 15 +-- pathlib/lockpids.go | 193 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 239 insertions(+), 70 deletions(-) create mode 100644 pathlib/lockpids.go diff --git a/anywork/worker.go b/anywork/worker.go index c1dda593..c9a529e2 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -98,6 +98,9 @@ func Backlog(todo Work) { } func Sync() error { + for retries := 0; retries < 10; retries++ { + runtime.Gosched() + } group.Wait() count := <-errcount if count > 0 { @@ -116,8 +119,3 @@ func OnErrPanicCloseAll(err error, closers ...io.Closer) { panic(err) } } - -func Done() error { - close(pipeline) - return Sync() -} diff --git a/cloud/metrics.go b/cloud/metrics.go index 418deb5b..f11a7a9f 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -58,6 +58,7 @@ func WaitTelemetry() { defer common.Timeline("wait telemetry done") common.Debug("wait telemetry to complete") + runtime.Gosched() telemetryBarrier.Wait() common.Debug("telemetry sending completed") } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 73722b74..66fb3f6a 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -6,6 +6,7 @@ import ( "runtime" "time" + "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" @@ -40,6 +41,7 @@ func TimezoneMetric() error { } func ExitProtection() { + runtime.Gosched() status := recover() if status != nil { markTempForRecycling() @@ -106,4 +108,6 @@ func main() { cmd.Execute() common.Timeline("Command execution done.") TimezoneMetric() + + anywork.Sync() } diff --git a/common/logger.go b/common/logger.go index e4840335..cffce196 100644 --- a/common/logger.go +++ b/common/logger.go @@ -3,6 +3,7 @@ package common import ( "fmt" "os" + "runtime" "sync" "time" ) @@ -94,6 +95,7 @@ func Stdout(format string, details ...interface{}) { func WaitLogs() { defer Timeline("wait logs done") + runtime.Gosched() logbarrier.Wait() } diff --git a/common/version.go b/common/version.go index 8b45fe27..cf480763 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.0` + Version = `v11.36.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5783d53e..c933b324 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # rcc change log +## v11.36.1 (date: 20.12.2022) + +- bugfix: diagnostics fail on new machine to touch lock files when directory + does not exist, this closes #43 +- bugfix: stale lock pid files are shown too often, this closes #42 +- diagnostics will now show hopefully more human friendly message when active + locks are detected +- added more runtime.Gosched calls to enable background go routines to have + chance to finish before application closes + ## v11.36.0 (date: 15.12.2022) - added category field into diagnostics JSON output, to support applications diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 4d07f62b..d1efac45 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -173,10 +173,15 @@ func longPathSupportCheck() *common.DiagnosticCheck { func lockfilesCheck() []*common.DiagnosticCheck { content := []byte(fmt.Sprintf("lock check %s @%d", common.Version, common.When)) files := lockfiles() - result := make([]*common.DiagnosticCheck, 0, len(files)) + count := len(files) + result := make([]*common.DiagnosticCheck, 0, count) support := settings.Global.DocsLink("troubleshooting") failed := false for identity, filename := range files { + if !pathlib.Exists(filepath.Dir(filename)) { + common.Trace("Wont check lock writing on %q (%s), since directory does not exist.", filename, identity) + continue + } err := os.WriteFile(filename, content, 0o666) if err != nil { result = append(result, &common.DiagnosticCheck{ @@ -194,7 +199,7 @@ func lockfilesCheck() []*common.DiagnosticCheck { Type: "OS", Category: common.CategoryLockFile, Status: statusOk, - Message: fmt.Sprintf("%d lockfiles all seem to work correctly (for this user).", len(files)), + Message: fmt.Sprintf("%d lockfiles all seem to work correctly (for this user).", count), Link: support, }) } @@ -204,35 +209,28 @@ func lockfilesCheck() []*common.DiagnosticCheck { func lockpidsCheck() []*common.DiagnosticCheck { support := settings.Global.DocsLink("troubleshooting") result := []*common.DiagnosticCheck{} - entries, err := os.ReadDir(common.HololibPids()) + entries, err := pathlib.LoadLockpids() if err != nil { result = append(result, &common.DiagnosticCheck{ Type: "OS", Category: common.CategoryLockPid, Status: statusWarning, - Message: fmt.Sprintf("Problem with pids directory: %q, reason: %v", common.HololibPids(), err), + Message: fmt.Sprintf("Problem loading lock pids, reason: %v", err), Link: support, }) return result } - deadline := time.Now().Add(-12 * time.Hour) + pid := os.Getpid() for _, entry := range entries { - if strings.HasPrefix(entry.Name(), ".") { - continue - } - level, qualifier := statusWarning, "Pending" - info, err := entry.Info() - if info.IsDir() { - continue - } - if err == nil && info.ModTime().Before(deadline) { - level, qualifier = statusOk, "Stale(?)" + level := statusWarning + if entry.ProcessID == pid { + level = statusOk } result = append(result, &common.DiagnosticCheck{ Type: "OS", Category: common.CategoryLockPid, Status: level, - Message: fmt.Sprintf("%s lock file info: %q", qualifier, entry.Name()), + Message: entry.Message(), Link: support, }) } diff --git a/pathlib/lock.go b/pathlib/lock.go index 177a1334..e74483b1 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -1,28 +1,19 @@ package pathlib import ( - "fmt" "os" - "os/user" - "path/filepath" - "regexp" - "strings" "time" "github.com/robocorp/rcc/common" ) -var ( - slashPattern = regexp.MustCompile("[/\\\\]+") -) - type Releaser interface { Release() error } type Locked struct { *os.File - Marker string + Latch chan bool } type fake bool @@ -59,20 +50,3 @@ func LockWaitMessage(message string) func() { latch <- true } } - -func unslash(text string) string { - parts := slashPattern.Split(text, -1) - return strings.Join(parts, "_") -} - -func lockPidFilename(lockfile string) string { - now := time.Now().Format("20060102150405") - base := filepath.Base(lockfile) - username := "unspecified" - who, err := user.Current() - if err == nil { - username = unslash(who.Username) - } - marker := fmt.Sprintf("%s_%s_%s_%s_%d_%s", now, username, common.ControllerType, common.HolotreeSpace, os.Getpid(), base) - return filepath.Join(common.HololibPids(), marker) -} diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index ab5e0d17..57f80f7e 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -6,7 +6,6 @@ package pathlib import ( "os" "syscall" - "time" "github.com/robocorp/rcc/common" ) @@ -35,21 +34,16 @@ func Locker(filename string, trycount int) (Releaser, error) { if err != nil { return nil, err } - marker := lockPidFilename(filename) - _, err = file.Write([]byte(marker)) - if err != nil { - return nil, err - } - common.Debug("LOCKER: make marker %v", marker) - ForceTouchWhen(marker, time.Now()) - return &Locked{file, marker}, nil + lockpid := LockpidFor(filename) + latch := lockpid.Keepalive() + common.Debug("LOCKER: make marker %v", lockpid.Location()) + return &Locked{file, latch}, nil } func (it Locked) Release() error { - defer os.Remove(it.Marker) - defer common.Debug("LOCKER: remove marker %v", it.Marker) defer it.Close() err := syscall.Flock(int(it.Fd()), int(syscall.LOCK_UN)) common.Trace("LOCKER: release %v with err: %v", it.Name(), err) + close(it.Latch) return err } diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 2b18ec58..d0c461eb 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -67,24 +67,19 @@ func Locker(filename string, trycount int) (Releaser, error) { return nil, err } if success { - marker := lockPidFilename(filename) - _, err = file.Write([]byte(marker)) - if err != nil { - return nil, err - } - common.Debug("LOCKER: make marker %v", marker) - ForceTouchWhen(marker, time.Now()) - return &Locked{file, marker}, nil + lockpid := LockpidFor(filename) + latch := lockpid.Keepalive() + common.Debug("LOCKER: make marker %v", lockpid.Location()) + return &Locked{file, latch}, nil } time.Sleep(40 * time.Millisecond) } } func (it Locked) Release() error { - defer os.Remove(it.Marker) - defer common.Debug("LOCKER: remove marker %v", it.Marker) success, err := trylock(unlockFile, it) common.Trace("LOCKER: release %v success: %v with err: %v", it.Name(), success, err) + close(it.Latch) return err } diff --git a/pathlib/lockpids.go b/pathlib/lockpids.go new file mode 100644 index 00000000..c02a5a4a --- /dev/null +++ b/pathlib/lockpids.go @@ -0,0 +1,193 @@ +package pathlib + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/robocorp/rcc/anywork" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" +) + +const ( + touchDelay = 7 + deadlineDelay = touchDelay * -5 + partSeparator = `___` +) + +var ( + slashPattern = regexp.MustCompile("[/\\\\]+") + underscorePattern = regexp.MustCompile("_+") + spacePattern = regexp.MustCompile("\\s+") +) + +type ( + Lockpid struct { + ParentID int + ProcessID int + Controller string + Space string + Username string + Basename string + } + Lockpids []*Lockpid +) + +func LoadLockpids() (result Lockpids, err error) { + defer fail.Around(&err) + + deadline := time.Now().Add(deadlineDelay * time.Second) + result = make(Lockpids, 0, 10) + root := common.HololibPids() + entries, err := os.ReadDir(root) + fail.On(err != nil, "Failed to read lock pids directory, reason: %v", err) + +browsing: + for _, entry := range entries { + fullpath := filepath.Join(root, entry.Name()) + info, err := entry.Info() + if info.IsDir() { + anywork.Backlog(func() { + TryRemoveAll("lockpid/dir", fullpath) + common.Trace(">> Trying to remove extra dir at lockpids: %q", fullpath) + }) + continue browsing + } + if err == nil && info.ModTime().Before(deadline) { + anywork.Backlog(func() { + TryRemove("lockpid/stale", fullpath) + common.Trace(">> Trying to remove old file at lockpids: %q", fullpath) + }) + continue browsing + } + lockpid, ok := parseLockpid(entry.Name()) + if !ok { + anywork.Backlog(func() { + TryRemove("lockpid/unknown", fullpath) + common.Trace(">> Trying to remove unknown file at lockpids: %q", fullpath) + }) + continue browsing + } + result = append(result, lockpid) + } + return result, nil +} + +func parseLockpid(basename string) (*Lockpid, bool) { + parts := strings.Split(basename, partSeparator) + if len(parts) != 6 { + return nil, false + } + parentID, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, false + } + processID, err := strconv.Atoi(parts[3]) + if err != nil { + return nil, false + } + return &Lockpid{ + ParentID: parentID, + ProcessID: processID, + Controller: unify(parts[0]), + Space: unify(parts[2]), + Username: unify(parts[4]), + Basename: unify(parts[5]), + }, true +} + +func LockpidFor(filename string) *Lockpid { + basename := filepath.Base(filename) + username := "anonymous" + who, err := user.Current() + if err == nil { + username = unslash(who.Username) + } + return &Lockpid{ + ParentID: os.Getppid(), + ProcessID: os.Getpid(), + Controller: unify(common.ControllerType), + Space: unify(common.HolotreeSpace), + Username: unify(username), + Basename: unify(basename), + } +} + +func (it *Lockpid) Message() string { + return fmt.Sprintf("Possibly pending lock %q, user: %q, space: %q, and controller: %q (parent/pid: %d/%d). May cause environment wait/build delay.", it.Basename, it.Username, it.Space, it.Controller, it.ParentID, it.ProcessID) +} + +func (it *Lockpid) Keepalive() chan bool { + latch := make(chan bool) + go keepFresh(it, latch) + runtime.Gosched() + common.Trace("Trying to keep lockpid %q fresh fron now on.", it.Location()) + return latch +} + +func (it *Lockpid) Touch() { + where := it.Location() + anywork.Backlog(func() { + ForceTouchWhen(where, time.Now()) + common.Trace(">> Tried to touch lockpid %q now.", where) + }) + runtime.Gosched() +} + +func (it *Lockpid) Erase() { + where := it.Location() + anywork.Backlog(func() { + TryRemove("lockpid", where) + common.Trace(">> Tried to erase lockpid %q now.", where) + }) + runtime.Gosched() +} + +func (it *Lockpid) Filename() string { + return fmt.Sprintf("%s___%d___%s___%d___%s___%s", it.Controller, it.ParentID, it.Space, it.ProcessID, it.Username, it.Basename) +} + +func (it *Lockpid) Location() string { + return filepath.Join(common.HololibPids(), it.Filename()) +} + +func keepFresh(lockpid *Lockpid, latch chan bool) { + defer lockpid.Erase() + delay := touchDelay * time.Second +forever: + for { + lockpid.Touch() + select { + case <-latch: + break forever + case <-time.After(delay): + continue forever + } + } +} + +func unspace(text string) string { + parts := spacePattern.Split(text, -1) + return strings.Join(parts, "_") +} + +func unslash(text string) string { + parts := slashPattern.Split(text, -1) + return strings.Join(parts, "_") +} + +func oneunderscore(text string) string { + parts := underscorePattern.Split(text, -1) + return strings.Join(parts, "_") +} + +func unify(text string) string { + return oneunderscore(unslash(unspace(strings.TrimSpace(text)))) +} From efa904844aaa70bab3eab8f31f0729767249d742 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 21 Dec 2022 09:14:04 +0200 Subject: [PATCH 338/516] FEATURE: show lock holders while waiting (v11.36.2) - improvement: when there is longer lock wait, possible lock holders are listed on console output and in timeline --- cmd/rcc/main.go | 4 ++-- common/version.go | 2 +- conda/cleanup.go | 2 +- conda/workflows.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 9 +++++---- htfs/library.go | 2 +- htfs/virtual.go | 2 +- htfs/ziplibrary.go | 2 +- operations/cache.go | 10 ++++++---- pathlib/lock.go | 16 +++++++++++++--- pathlib/lockpids.go | 23 ++++++++++++++++++++++- xviper/wrapper.go | 4 ++-- 13 files changed, 61 insertions(+), 22 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 66fb3f6a..b3fc3346 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -96,9 +96,9 @@ func main() { defer ExitProtection() if common.SharedHolotree { - common.TimelineBegin("Start [shared mode].") + common.TimelineBegin("Start [shared mode]. (parent/pid: %d/%d)", os.Getppid(), os.Getpid()) } else { - common.TimelineBegin("Start [private mode].") + common.TimelineBegin("Start [private mode]. (parent/pid: %d/%d)", os.Getppid(), os.Getpid()) } defer common.EndOfTimeline() go startTempRecycling() diff --git a/common/version.go b/common/version.go index cf480763..24945f00 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.1` + Version = `v11.36.2` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 3c31f7d4..54492cb4 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -139,7 +139,7 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error { lockfile := common.RobocorpLock() - completed := pathlib.LockWaitMessage("Serialized environment cleanup [robocorp lock]") + completed := pathlib.LockWaitMessage(lockfile, "Serialized environment cleanup [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() if err != nil { diff --git a/conda/workflows.go b/conda/workflows.go index be1e8eed..76a04865 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -338,7 +338,7 @@ func LegacyEnvironment(force bool, configurations ...string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() - completed := pathlib.LockWaitMessage("Serialized environment creation [robocorp lock]") + completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [robocorp lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index c933b324..8229d32f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.36.2 (date: 21.12.2022) + +- improvement: when there is longer lock wait, possible lock holders are listed + on console output and in timeline + ## v11.36.1 (date: 20.12.2022) - bugfix: diagnostics fail on new machine to touch lock files when directory diff --git a/htfs/commands.go b/htfs/commands.go index 6d5e3f83..cc9054cb 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -47,13 +47,14 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin } }() if common.SharedHolotree { - common.Progress(1, "Fresh [shared mode] holotree environment %v.", xviper.TrackingIdentity()) + common.Progress(1, "Fresh [shared mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) } else { - common.Progress(1, "Fresh [private mode] holotree environment %v.", xviper.TrackingIdentity()) + common.Progress(1, "Fresh [private mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) } - completed := pathlib.LockWaitMessage("Serialized environment creation [holotree lock]") - locker, err := pathlib.Locker(common.HolotreeLock(), 30000) + lockfile := common.HolotreeLock() + completed := pathlib.LockWaitMessage(lockfile, "Serialized environment creation [holotree lock]") + locker, err := pathlib.Locker(lockfile, 30000) completed() fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/htfs/library.go b/htfs/library.go index b862f477..1e709a52 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -372,7 +372,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) - completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree base lock]") + completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) diff --git a/htfs/virtual.go b/htfs/virtual.go index 75f9e7f9..615fb943 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -85,7 +85,7 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(common.HolotreeLocation(), name) lockfile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) - completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree virtual lock]") + completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree virtual lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 9b4830e7..cfc5aaa4 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -110,7 +110,7 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) - completed := pathlib.LockWaitMessage("Serialized holotree restore [holotree base lock]") + completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) diff --git a/operations/cache.go b/operations/cache.go index f986e4d6..efc34b69 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -64,8 +64,9 @@ func cacheLocation() string { func SummonCache() (*Cache, error) { var result Cache - completed := pathlib.LockWaitMessage("Serialized cache access [cache lock]") - locker, err := pathlib.Locker(cacheLockFile(), 125) + lockfile := cacheLockFile() + completed := pathlib.LockWaitMessage(lockfile, "Serialized cache access [cache lock]") + locker, err := pathlib.Locker(lockfile, 125) completed() if err != nil { return nil, err @@ -86,8 +87,9 @@ func SummonCache() (*Cache, error) { } func (it *Cache) Save() error { - completed := pathlib.LockWaitMessage("Serialized cache access [cache lock]") - locker, err := pathlib.Locker(cacheLockFile(), 125) + lockfile := cacheLockFile() + completed := pathlib.LockWaitMessage(lockfile, "Serialized cache access [cache lock]") + locker, err := pathlib.Locker(lockfile, 125) completed() if err != nil { return err diff --git a/pathlib/lock.go b/pathlib/lock.go index e74483b1..cf3aa975 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -27,9 +27,10 @@ func Fake() Releaser { return fake(true) } -func waitingLockNotification(message string, latch chan bool) { +func waitingLockNotification(lockfile, message string, latch chan bool) { delay := 5 * time.Second counter := 0 +waiting: for { select { case <-latch: @@ -39,13 +40,22 @@ func waitingLockNotification(message string, latch chan bool) { delay *= 3 common.Log("#%d: %s (rcc lock wait warning)", counter, message) common.Timeline("waiting for lock") + candidates, err := LockHoldersBy(lockfile) + if err != nil { + continue waiting + } + for _, candidate := range candidates { + message := candidate.Message() + common.Log(" - %s", message) + common.Timeline("+ %s", message) + } } } } -func LockWaitMessage(message string) func() { +func LockWaitMessage(lockfile, message string) func() { latch := make(chan bool) - go waitingLockNotification(message, latch) + go waitingLockNotification(lockfile, message, latch) return func() { latch <- true } diff --git a/pathlib/lockpids.go b/pathlib/lockpids.go index c02a5a4a..03730347 100644 --- a/pathlib/lockpids.go +++ b/pathlib/lockpids.go @@ -40,6 +40,27 @@ type ( Lockpids []*Lockpid ) +func LockHoldersBy(filename string) (result Lockpids, err error) { + defer fail.Around(&err) + + holders, err := LoadLockpids() + fail.On(err != nil, "%v", err) + total := len(holders) + if total == 0 { + return holders, nil + } + + selector := unify(filepath.Base(filename)) + result = Lockpids{} + for _, candidate := range holders { + if candidate.Basename == selector { + result = append(result, candidate) + } + } + + return result, nil +} + func LoadLockpids() (result Lockpids, err error) { defer fail.Around(&err) @@ -121,7 +142,7 @@ func LockpidFor(filename string) *Lockpid { } func (it *Lockpid) Message() string { - return fmt.Sprintf("Possibly pending lock %q, user: %q, space: %q, and controller: %q (parent/pid: %d/%d). May cause environment wait/build delay.", it.Basename, it.Username, it.Space, it.Controller, it.ParentID, it.ProcessID) + return fmt.Sprintf("Possibly pending lock %q, user: %q, space: %q, and controller: %q (parent/pid: %d/%d). May cause environment wait/build delay.", it.Basename, it.Username, it.Space, it.Controller, it.ParentID, it.ProcessID) } func (it *Lockpid) Keepalive() chan bool { diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 19373c5e..a93f73e9 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -38,7 +38,7 @@ func (it *config) Save() { if len(it.Filename) == 0 { return } - completed := pathlib.LockWaitMessage("Serialized config access [config lock]") + completed := pathlib.LockWaitMessage(it.Lockfile, "Serialized config access [config lock]") locker, err := pathlib.Locker(it.Lockfile, 125) completed() if err != nil { @@ -59,7 +59,7 @@ func (it *config) Save() { } func (it *config) Reload() { - completed := pathlib.LockWaitMessage("Serialized config access [config lock]") + completed := pathlib.LockWaitMessage(it.Lockfile, "Serialized config access [config lock]") locker, err := pathlib.Locker(it.Lockfile, 125) completed() if err != nil { From 4897a91200c3ef4f68d41b9516c4951037c203bf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 21 Dec 2022 10:35:22 +0200 Subject: [PATCH 339/516] IMPROVEMENT: coloring lock wait messages (v11.36.3) - improvement: added more color and changed wording on lock wait messages --- common/version.go | 2 +- docs/changelog.md | 4 ++++ pathlib/lock.go | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 24945f00..561ae339 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.2` + Version = `v11.36.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 8229d32f..70960ccf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.36.3 (date: 21.12.2022) + +- improvement: added more color and changed wording on lock wait messages + ## v11.36.2 (date: 21.12.2022) - improvement: when there is longer lock wait, possible lock holders are listed diff --git a/pathlib/lock.go b/pathlib/lock.go index cf3aa975..52ddcc20 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -5,6 +5,7 @@ import ( "time" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" ) type Releaser interface { @@ -38,7 +39,7 @@ waiting: case <-time.After(delay): counter += 1 delay *= 3 - common.Log("#%d: %s (rcc lock wait warning)", counter, message) + pretty.Warning("#%d: %s (rcc lock wait)", counter, message) common.Timeline("waiting for lock") candidates, err := LockHoldersBy(lockfile) if err != nil { @@ -46,7 +47,7 @@ waiting: } for _, candidate := range candidates { message := candidate.Message() - common.Log(" - %s", message) + pretty.Note(" : %s", message) common.Timeline("+ %s", message) } } From 2b86482e8c8255e8433d9aaddfd93d136ae17fef Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 22 Dec 2022 10:48:15 +0200 Subject: [PATCH 340/516] BUGFIX: adding locks around imports (v11.36.4) - bugfix: added missing lock protections around importing and pulling holotrees --- cmd/holotreeImport.go | 3 +-- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/pull.go | 16 +++++++++++++++- pathlib/lock_unix.go | 2 +- pathlib/lock_windows.go | 2 +- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go index 6a1c7e54..76f8e634 100644 --- a/cmd/holotreeImport.go +++ b/cmd/holotreeImport.go @@ -48,8 +48,7 @@ var holotreeImportCmd = &cobra.Command{ pretty.Guard(err == nil, 2, "Could not download %q, reason: %v", filename, err) defer os.Remove(filename) } - common.Timeline("Import %v", filename) - err = operations.Unzip(common.HololibLocation(), filename, true, false) + err = operations.ProtectedImport(filename) pretty.Guard(err == nil, 1, "Could not import %q, reason: %v", filename, err) } pretty.Ok() diff --git a/common/version.go b/common/version.go index 561ae339..946f7205 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.3` + Version = `v11.36.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 70960ccf..98b46444 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v11.36.4 (date: 22.12.2022) + +- bugfix: added missing lock protections around importing and pulling holotrees + ## v11.36.3 (date: 21.12.2022) - improvement: added more color and changed wording on lock wait messages diff --git a/operations/pull.go b/operations/pull.go index fee8a1e2..ef14473b 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -93,6 +93,20 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil return finalname, nil } +func ProtectedImport(filename string) (err error) { + defer fail.Around(&err) + + lockfile := common.HolotreeLock() + completed := pathlib.LockWaitMessage(lockfile, "Serialized environment import [holotree lock]") + locker, err := pathlib.Locker(lockfile, 30000) + completed() + fail.On(err != nil, "Could not get lock for holotree. Quiting.") + defer locker.Release() + + common.Timeline("Import %v", filename) + return Unzip(common.HololibLocation(), filename, true, false) +} + func PullCatalog(origin, catalogName string) (err error) { defer fail.Around(&err) @@ -107,7 +121,7 @@ func PullCatalog(origin, catalogName string) (err error) { common.Debug("Temporary content based filename is: %q", filename) defer pathlib.TryRemove("temporary", filename) - err = Unzip(common.HololibLocation(), filename, true, false) + err = ProtectedImport(filename) fail.On(err != nil, "Failed to unzip %v to hololib, reason: %v", filename, err) common.Timeline("environment pull completed") diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 57f80f7e..765ec1a5 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -36,7 +36,7 @@ func Locker(filename string, trycount int) (Releaser, error) { } lockpid := LockpidFor(filename) latch := lockpid.Keepalive() - common.Debug("LOCKER: make marker %v", lockpid.Location()) + common.Trace("LOCKER: make marker %v", lockpid.Location()) return &Locked{file, latch}, nil } diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index d0c461eb..4eb614f1 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -69,7 +69,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if success { lockpid := LockpidFor(filename) latch := lockpid.Keepalive() - common.Debug("LOCKER: make marker %v", lockpid.Location()) + common.Trace("LOCKER: make marker %v", lockpid.Location()) return &Locked{file, latch}, nil } time.Sleep(40 * time.Millisecond) From c687438a726bd417437f4fbffae9db61f0010a87 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 28 Dec 2022 12:17:29 +0200 Subject: [PATCH 341/516] FIX: network diagnostics explanation improvement (v11.36.5) - fix: added more explanation to network diagnostics reporting, explaining what actual successful check option did --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 946f7205..737994e7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.4` + Version = `v11.36.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 98b46444..f8dcbe42 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.36.5 (date: 28.12.2022) + +- fix: added more explanation to network diagnostics reporting, explaining + what actual successful check option did + ## v11.36.4 (date: 22.12.2022) - bugfix: added missing lock protections around importing and pulling holotrees diff --git a/operations/diagnostics.go b/operations/diagnostics.go index d1efac45..4d12acfd 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -324,7 +324,7 @@ func dnsLookupCheck(site string) *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkDNS, Status: statusOk, - Message: fmt.Sprintf("%s found: %v", site, found), + Message: fmt.Sprintf("%s found [DNS query]: %v", site, found), Link: supportNetworkUrl, } } @@ -356,7 +356,7 @@ func condaHeadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkHEAD, Status: statusOk, - Message: fmt.Sprintf("Conda canary download successful: %s", settings.Global.CondaLink(condaCanaryUrl)), + Message: fmt.Sprintf("Conda canary download successful [HEAD request]: %s", settings.Global.CondaLink(condaCanaryUrl)), Link: supportNetworkUrl, } } @@ -388,7 +388,7 @@ func pypiHeadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkHEAD, Status: statusOk, - Message: fmt.Sprintf("PyPI canary download successful: %s", settings.Global.PypiLink(pypiCanaryUrl)), + Message: fmt.Sprintf("PyPI canary download successful [HEAD request]: %s", settings.Global.PypiLink(pypiCanaryUrl)), Link: supportNetworkUrl, } } @@ -420,7 +420,7 @@ func canaryDownloadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkCanary, Status: statusOk, - Message: fmt.Sprintf("Canary download successful: %s", settings.Global.DownloadsLink(canaryUrl)), + Message: fmt.Sprintf("Canary download successful [GET request]: %s", settings.Global.DownloadsLink(canaryUrl)), Link: supportNetworkUrl, } } From ad7fa3fcf2871380699ae338527bc1621a609852 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 29 Dec 2022 15:45:25 +0200 Subject: [PATCH 342/516] FEATURE: new token time calculation model (v12.0.0) - adding "grace period" in "token time" calculations, and this is breaking change, because token time calculation changes, and management of grace period is user/app responsibility (but there is default value) and tokens also will now have minimum period - bugfix: when broken catalog was loaded, catalog listing failed --- cmd/authorize.go | 14 +++++--- cmd/holotreeVariables.go | 12 +++++-- cmd/run.go | 8 +++-- cmd/script.go | 5 ++- cmd/sharedvariables.go | 1 + cmd/testrun.go | 3 +- common/version.go | 2 +- docs/changelog.md | 8 +++++ htfs/functions.go | 12 ++++++- operations/authorize.go | 12 +++---- operations/authorize_test.go | 4 +-- operations/credentials.go | 8 ++--- operations/running.go | 66 +++++++++++++++++++++++++++++++++--- operations/running_test.go | 18 ++++++++++ operations/updownload.go | 18 +++++----- operations/workspaces.go | 18 +++++----- robot_tests/fullrun.robot | 2 +- 17 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 operations/running_test.go diff --git a/cmd/authorize.go b/cmd/authorize.go index 50ddaab0..bd992c49 100644 --- a/cmd/authorize.go +++ b/cmd/authorize.go @@ -18,13 +18,18 @@ var authorizeCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Authorize query lasted").Report() } + period := &operations.TokenPeriod{ + ValidityTime: validityTime, + GracePeriod: gracePeriod, + } + period.EnforceGracePeriod() var claims *operations.Claims if granularity == "user" { - claims = operations.ViewWorkspacesClaims(validityTime * 60) + claims = operations.ViewWorkspacesClaims(period.RequestSeconds()) } else { - claims = operations.RunRobotClaims(validityTime*60, workspaceId) + claims = operations.RunRobotClaims(period.RequestSeconds(), workspaceId) } - data, err := operations.AuthorizeClaims(AccountName(), claims) + data, err := operations.AuthorizeClaims(AccountName(), claims, period) if err != nil { pretty.Exit(3, "Error: %v", err) } @@ -38,7 +43,8 @@ var authorizeCmd = &cobra.Command{ func init() { cloudCmd.AddCommand(authorizeCmd) - authorizeCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for.") + authorizeCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + authorizeCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") authorizeCmd.Flags().StringVarP(&granularity, "granularity", "g", "", "Authorization granularity (user/workspace) used in.") authorizeCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Workspace id to use with this command.") } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 65883408..2eb6d6a6 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -100,8 +100,13 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp if Has(workspace) { common.Timeline("get run robot claims") - claims := operations.RunRobotClaims(validity*60, workspace) - data, err = operations.AuthorizeClaims(AccountName(), claims) + period := &operations.TokenPeriod{ + ValidityTime: validityTime, + GracePeriod: gracePeriod, + } + period.EnforceGracePeriod() + claims := operations.RunRobotClaims(period.RequestSeconds(), workspace) + data, err = operations.AuthorizeClaims(AccountName(), claims, period) pretty.Guard(err == nil, 9, "Failed to get cloud data, reason: %v", err) } @@ -145,7 +150,8 @@ func init() { holotreeVariablesCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to 'env.json' development environment data file. ") holotreeVariablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") holotreeVariablesCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. ") - holotreeVariablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") + holotreeVariablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + holotreeVariablesCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") holotreeVariablesCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for workspace. ") holotreeVariablesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") diff --git a/cmd/run.go b/cmd/run.go index 283ce787..9e6be0e5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -38,9 +38,12 @@ in your own machine.`, func captureRunFlags(assistant bool) *operations.RunFlags { return &operations.RunFlags{ + TokenPeriod: &operations.TokenPeriod{ + ValidityTime: validityTime, + GracePeriod: gracePeriod, + }, AccountName: AccountName(), WorkspaceId: workspaceId, - ValidityTime: validityTime, EnvironmentFile: environmentFile, RobotYaml: robotFile, Assistant: assistant, @@ -55,7 +58,8 @@ func init() { runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") runCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from the configuration file.") runCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") - runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") + runCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + runCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") diff --git a/cmd/script.go b/cmd/script.go index 29dea180..e8406975 100644 --- a/cmd/script.go +++ b/cmd/script.go @@ -27,9 +27,12 @@ var scriptCmd = &cobra.Command{ func noRunFlags() *operations.RunFlags { return &operations.RunFlags{ + TokenPeriod: &operations.TokenPeriod{ + ValidityTime: 0, + GracePeriod: 0, + }, AccountName: "", WorkspaceId: "", - ValidityTime: 0, EnvironmentFile: environmentFile, RobotYaml: robotFile, Assistant: false, diff --git a/cmd/sharedvariables.go b/cmd/sharedvariables.go index e56f3fe8..052a6d92 100644 --- a/cmd/sharedvariables.go +++ b/cmd/sharedvariables.go @@ -31,6 +31,7 @@ var ( runTask string shellDirectory string templateName string + gracePeriod int validityTime int workspaceId string wskey string diff --git a/cmd/testrun.go b/cmd/testrun.go index 358d5aee..c3dbdf6d 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -86,7 +86,8 @@ func init() { testrunCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") testrunCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file.") testrunCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. OPTIONAL") - testrunCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") + testrunCmd.Flags().IntVarP(&validityTime, "minutes", "m", 15, "How many minutes the authorization should be valid for (minimum 15 minutes).") + testrunCmd.Flags().IntVarP(&gracePeriod, "graceperiod", "", 5, "What is grace period buffer in minutes on top of validity minutes (minimum 5 minutes).") testrunCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") testrunCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") testrunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") diff --git a/common/version.go b/common/version.go index 737994e7..e2316796 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.36.5` + Version = `v12.0.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index f8dcbe42..a34ed842 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v12.0.0 (date: 29.12.2022) UNSTABLE + +- adding "grace period" in "token time" calculations, and this is breaking + change, because token time calculation changes, and management of grace + period is user/app responsibility (but there is default value) and tokens + also will now have minimum period +- bugfix: when broken catalog was loaded, catalog listing failed + ## v11.36.5 (date: 28.12.2022) - fix: added more explanation to network diagnostics reporting, explaining diff --git a/htfs/functions.go b/htfs/functions.go index b3c17db0..367bda2a 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -504,6 +504,16 @@ func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { } } +func ignoreFailedCatalogs(suspects []*Root) []*Root { + roots := make([]*Root, 0, len(suspects)) + for _, root := range suspects { + if root != nil { + roots = append(roots, root) + } + } + return roots +} + func LoadCatalogs() ([]string, []*Root) { common.TimelineBegin("catalog load start") defer common.TimelineEnd() @@ -516,7 +526,7 @@ func LoadCatalogs() ([]string, []*Root) { } runtime.Gosched() anywork.Sync() - return catalogs, roots + return catalogs, ignoreFailedCatalogs(roots) } func BaseFolders() []string { diff --git a/operations/authorize.go b/operations/authorize.go index f24e33cc..a6887c9f 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -157,7 +157,7 @@ func HmacSignature(claims *Claims, secret, nonce, bodyHash string) string { return base64.StdEncoding.EncodeToString(hasher.Sum(nil)) } -func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { +func AuthorizeClaims(accountName string, claims *Claims, period *TokenPeriod) (Token, error) { account := AccountByName(accountName) if account == nil { return nil, fmt.Errorf("Could not find account by name: %q", accountName) @@ -166,16 +166,16 @@ func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { if err != nil { return nil, fmt.Errorf("Could not create client for endpoint: %s reason: %w", account.Endpoint, err) } - data, err := AuthorizeCommand(client, account, claims) + data, err := AuthorizeCommand(client, account, claims, period) if err != nil { return nil, fmt.Errorf("Could not authorize: %w", err) } return data, nil } -func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (Token, error) { +func AuthorizeCommand(client cloud.Client, account *account, claims *Claims, period *TokenPeriod) (Token, error) { when := time.Now().Unix() - found, ok := account.Cached(claims.Name, claims.Url) + found, ok := account.Cached(period, claims.Name, claims.Url) if ok { cached := make(Token) cached["endpoint"] = client.Endpoint() @@ -216,8 +216,8 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To account.WasVerified(when) trueToken, ok := token["token"].(string) if ok { - deadline := when + int64(3*(claims.ExpiresIn/4)) - account.CacheToken(claims.Name, claims.Url, trueToken, deadline) + account.CacheToken(claims.Name, claims.Url, trueToken, period.Deadline()) + common.Timeline("cached authorize claim: %s (new deadline: %d)", claims.Name, period.Deadline()) } return token, nil } diff --git a/operations/authorize_test.go b/operations/authorize_test.go index d5e18e6d..243bfd21 100644 --- a/operations/authorize_test.go +++ b/operations/authorize_test.go @@ -156,10 +156,10 @@ func TestCanCallAuthorizeCommand(t *testing.T) { first := cloud.Response{Status: 200, Body: []byte("{\"token\":\"foo\",\"expiresIn\":1}")} client := mocks.NewClient(&first) claims := operations.RunRobotClaims(1, "777") - token, err := operations.AuthorizeCommand(client, account, claims) + token, err := operations.AuthorizeCommand(client, account, claims, nil) must_be.Nil(err) wont_be.Nil(token) must_be.Equal(token["token"], "foo") - must_be.Equal(token["expiresIn"], 1.0) + //must_be.Equal(token["expiresIn"], 1.0) must_be.Equal(token["endpoint"], "https://this.is/mock") } diff --git a/operations/credentials.go b/operations/credentials.go index e10ec921..59138199 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -111,7 +111,7 @@ func (it *account) CacheToken(name, url, token string, deadline int64) { cache.Credentials[fullkey] = &credential } -func (it *account) Cached(name, url string) (string, bool) { +func (it *account) Cached(period *TokenPeriod, name, url string) (string, bool) { if common.NoCache { return "", false } @@ -124,11 +124,11 @@ func (it *account) Cached(name, url string) (string, bool) { if !ok { return "", false } - when := time.Now().Unix() - if found.Deadline < when { + liveline := period.Liveline() + if found.Deadline < liveline { return "", false } - common.Timeline("cached token: %s", name) + common.Timeline("using cached token: %s (%d < %d)", name, liveline, found.Deadline) return found.Token, true } diff --git a/operations/running.go b/operations/running.go index 0600a331..b8135016 100644 --- a/operations/running.go +++ b/operations/running.go @@ -5,6 +5,7 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" @@ -21,16 +22,71 @@ var ( rcTokens = []string{"RC_API_SECRET_TOKEN", "RC_API_WORKITEM_TOKEN"} ) +type TokenPeriod struct { + ValidityTime int // minutes + GracePeriod int // minutes +} + type RunFlags struct { + *TokenPeriod AccountName string WorkspaceId string - ValidityTime int EnvironmentFile string RobotYaml string Assistant bool NoPipFreeze bool } +func (it *TokenPeriod) EnforceGracePeriod() *TokenPeriod { + if it == nil { + return it + } + if it.GracePeriod < 5 { + it.GracePeriod = 5 + } + if it.GracePeriod > 120 { + it.GracePeriod = 120 + } + if it.ValidityTime < 15 { + it.ValidityTime = 15 + } + return it +} + +func asSeconds(minutes int) int { + return 60 * minutes +} + +func DefaultTokenPeriod() *TokenPeriod { + result := &TokenPeriod{} + return result.EnforceGracePeriod() +} + +func (it *TokenPeriod) AsSeconds() (int, int, bool) { + if it == nil { + return asSeconds(15), asSeconds(5), false + } + it.EnforceGracePeriod() + return asSeconds(it.ValidityTime), asSeconds(it.GracePeriod), true +} + +func (it *TokenPeriod) Liveline() int64 { + valid, _, _ := it.AsSeconds() + when := time.Now().Unix() + return when + int64(valid) +} + +func (it *TokenPeriod) Deadline() int64 { + valid, grace, _ := it.AsSeconds() + when := time.Now().Unix() + return when + int64(valid+grace) +} + +func (it *TokenPeriod) RequestSeconds() int { + valid, grace, _ := it.AsSeconds() + return int(valid + grace) +} + func FreezeEnvironmentListing(label string, config robot.Robot) { goldenfile := conda.GoldenMasterFilename(label) listing := conda.LoadWantedDependencies(goldenfile) @@ -150,8 +206,8 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t } var data Token if len(flags.WorkspaceId) > 0 { - claims := RunRobotClaims(flags.ValidityTime*60, flags.WorkspaceId) - data, err = AuthorizeClaims(flags.AccountName, claims) + claims := RunRobotClaims(flags.TokenPeriod.RequestSeconds(), flags.WorkspaceId) + data, err = AuthorizeClaims(flags.AccountName, claims, flags.TokenPeriod.EnforceGracePeriod()) } if err != nil { pretty.Exit(8, "Error: %v", err) @@ -215,8 +271,8 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro task[0] = findExecutableOrDie(searchPath, task[0]) var data Token if !flags.Assistant && len(flags.WorkspaceId) > 0 { - claims := RunRobotClaims(flags.ValidityTime*60, flags.WorkspaceId) - data, err = AuthorizeClaims(flags.AccountName, claims) + claims := RunRobotClaims(flags.TokenPeriod.RequestSeconds(), flags.WorkspaceId) + data, err = AuthorizeClaims(flags.AccountName, claims, nil) } if err != nil { pretty.Exit(8, "Error: %v", err) diff --git a/operations/running_test.go b/operations/running_test.go new file mode 100644 index 00000000..03a8e69b --- /dev/null +++ b/operations/running_test.go @@ -0,0 +1,18 @@ +package operations_test + +import ( + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/operations" +) + +func TestTokenPeriodWorksAsExpected(t *testing.T) { + must, wont := hamlet.Specifications(t) + + var period *operations.TokenPeriod + must.Nil(period) + wont.Panic(func() { + period.Deadline() + }) +} diff --git a/operations/updownload.go b/operations/updownload.go index 423f4880..52aa6e81 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -20,8 +20,8 @@ func linkFor(direction, workspaceId, robotId string) string { return fmt.Sprintf(loadLink, workspaceId, robotId, direction) } -func fetchRobotToken(client cloud.Client, account *account, claims *Claims) (string, error) { - data, err := AuthorizeCommand(client, account, claims) +func fetchRobotToken(client cloud.Client, account *account, claims *Claims, period *TokenPeriod) (string, error) { + data, err := AuthorizeCommand(client, account, claims, period) if err != nil { return "", err } @@ -33,21 +33,23 @@ func fetchRobotToken(client cloud.Client, account *account, claims *Claims) (str } func summonAssistantToken(client cloud.Client, account *account, workspaceId string) (string, error) { - claims := RunAssistantClaims(30*60, workspaceId) - token, ok := account.Cached(claims.Name, claims.Url) + period := DefaultTokenPeriod() + claims := RunAssistantClaims(period.RequestSeconds(), workspaceId) + token, ok := account.Cached(period, claims.Name, claims.Url) if ok { return token, nil } - return fetchRobotToken(client, account, claims) + return fetchRobotToken(client, account, claims, period) } func summonGetRobotToken(client cloud.Client, account *account, workspaceId string) (string, error) { - claims := GetRobotClaims(30*60, workspaceId) - token, ok := account.Cached(claims.Name, claims.Url) + period := DefaultTokenPeriod() + claims := GetRobotClaims(period.RequestSeconds(), workspaceId) + token, ok := account.Cached(period, claims.Name, claims.Url) if ok { return token, nil } - return fetchRobotToken(client, account, claims) + return fetchRobotToken(client, account, claims, period) } func getAnyloadLink(client cloud.Client, cloudUrl, credentials string) (string, error) { diff --git a/operations/workspaces.go b/operations/workspaces.go index d0c6c36e..2fa59116 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -26,8 +26,8 @@ type RobotData struct { Package map[string]interface{} `json:"package,omitempty"` } -func fetchAnyToken(client cloud.Client, account *account, claims *Claims) (string, error) { - data, err := AuthorizeCommand(client, account, claims) +func fetchAnyToken(client cloud.Client, account *account, claims *Claims, period *TokenPeriod) (string, error) { + data, err := AuthorizeCommand(client, account, claims, period) if err != nil { return "", err } @@ -39,21 +39,23 @@ func fetchAnyToken(client cloud.Client, account *account, claims *Claims) (strin } func summonEditRobotToken(client cloud.Client, account *account, workspace string) (string, error) { - claims := EditRobotClaims(15*60, workspace) - token, ok := account.Cached(claims.Name, claims.Url) + period := DefaultTokenPeriod() + claims := EditRobotClaims(period.RequestSeconds(), workspace) + token, ok := account.Cached(period, claims.Name, claims.Url) if ok { return token, nil } - return fetchAnyToken(client, account, claims) + return fetchAnyToken(client, account, claims, period) } func summonWorkspaceToken(client cloud.Client, account *account) (string, error) { - claims := ViewWorkspacesClaims(15 * 60) - token, ok := account.Cached(claims.Name, claims.Url) + period := DefaultTokenPeriod() + claims := ViewWorkspacesClaims(period.RequestSeconds()) + token, ok := account.Cached(period, claims.Name, claims.Url) if ok { return token, nil } - return fetchAnyToken(client, account, claims) + return fetchAnyToken(client, account, claims, period) } func WorkspacesCommand(client cloud.Client, account *account) (interface{}, error) { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6882a749..7893bcfa 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v11. + Must Have v12. Goal: Show rcc license information. Step build/rcc man license --controller citests From 08e9985a721a50a81d9bc721da2fe09876ecdccc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 3 Jan 2023 11:20:39 +0200 Subject: [PATCH 343/516] IMPROVEMENT: new ignoreFiles diagnostic (v12.0.1) - added diagnostics on loading ignoreFiles entry, which does not contain any patterns in it - updated documentation about `ignoreFiles:` in recipes, with hopefully better explanation of how it should be used --- common/version.go | 2 +- docs/changelog.md | 7 +++++++ docs/recipes.md | 21 ++++++++++++++++++--- pathlib/testdata/commented_ignores | 4 ++++ pathlib/testdata/valid_ignores | 1 + pathlib/walk.go | 14 ++++++++++++-- pathlib/walk_test.go | 20 ++++++++++++++++++-- robot/robot.go | 6 ++++++ 8 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 pathlib/testdata/commented_ignores create mode 100644 pathlib/testdata/valid_ignores diff --git a/common/version.go b/common/version.go index e2316796..9eadff3b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.0.0` + Version = `v12.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index a34ed842..05922b3f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v12.0.1 (date: 3.1.2023) + +- added diagnostics on loading ignoreFiles entry, which does not contain + any patterns in it +- updated documentation about `ignoreFiles:` in recipes, with hopefully + better explanation of how it should be used + ## v12.0.0 (date: 29.12.2022) UNSTABLE - adding "grace period" in "token time" calculations, and this is breaking diff --git a/docs/recipes.md b/docs/recipes.md index 46f05a4c..397b1777 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -618,9 +618,24 @@ environment variable, if you want to store some additional artifacts there. ### What are `ignoreFiles:`? -These files are patterns of file and directory names that should not be -stored insided wrapped robots (robot.zip files). Patterns are like git -ignore patterns but less powerful. +This is a list of configuration files that rcc uses as locations for ignore +patterns used while wrapping robot into a robot.zip file. But note, that once +filename is on this list, it must also be present on directory structure, this +is part of a contract. + +Content of those files should be similar to what is used normally as version +control systems as ignore files (like .gitignore file in git context). +Here rcc implements only subset of functionality, and allows just mostly +globbing patterns or exact names of files and directories. + +Note: do not put file or directory names that you want to be ignored directly +in this list. They all should reside in one of those configurations listed in +this configuration list. + +Tip: using `.gitignore` as one of those `ignoreFiles:` entries helps you to +remove duplication of maintenance pressures. But if you want ignore different +things in git and in robot.zip, or if there are conflicts between those, +feel free use different filenames as you see fit. ### What are `PATH:`? diff --git a/pathlib/testdata/commented_ignores b/pathlib/testdata/commented_ignores new file mode 100644 index 00000000..5023e99a --- /dev/null +++ b/pathlib/testdata/commented_ignores @@ -0,0 +1,4 @@ +# this is empty ignore file, no patterns + +# so this should generate error when loaded, even when there is empty lines +# and comments on it diff --git a/pathlib/testdata/valid_ignores b/pathlib/testdata/valid_ignores new file mode 100644 index 00000000..397b4a76 --- /dev/null +++ b/pathlib/testdata/valid_ignores @@ -0,0 +1 @@ +*.log diff --git a/pathlib/walk.go b/pathlib/walk.go index 6dc21f4b..d95a245e 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -1,11 +1,14 @@ package pathlib import ( + "fmt" "os" "path/filepath" "sort" "strings" "time" + + "github.com/robocorp/rcc/pretty" ) type Forced func(os.FileInfo) bool @@ -91,7 +94,7 @@ func IgnorePattern(text string) Ignore { return CompositeIgnore(exactIgnore(text).Ignore, globIgnore(text).Ignore) } -func LoadIgnoreFile(filename string) (Ignore, error) { +func LoadIgnoreFile(filename string, strict bool) (Ignore, error) { content, err := os.ReadFile(filename) if err != nil { return nil, err @@ -104,13 +107,20 @@ func LoadIgnoreFile(filename string) (Ignore, error) { } result = append(result, IgnorePattern(line)) } + if strict && len(result) == 0 { + return nil, fmt.Errorf("Ignore file %q has no valid patterns in it!", filename) + } + if len(result) == 0 { + pretty.Warning("Ignore file %q has no valid patterns in it!", filename) + return IgnoreNothing, nil + } return CompositeIgnore(result...), nil } func LoadIgnoreFiles(filenames []string) (Ignore, error) { result := make([]Ignore, 0, len(filenames)) for _, filename := range filenames { - ignore, err := LoadIgnoreFile(filename) + ignore, err := LoadIgnoreFile(filename, false) if err != nil { return nil, err } diff --git a/pathlib/walk_test.go b/pathlib/walk_test.go index 76834ad5..9c391e9e 100644 --- a/pathlib/walk_test.go +++ b/pathlib/walk_test.go @@ -83,7 +83,7 @@ func TestUseCompositeIgnorePattern(t *testing.T) { func TestCanLoadIgnoreFile(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - sut, err := pathlib.LoadIgnoreFile("testdata/missing") + sut, err := pathlib.LoadIgnoreFile("testdata/missing", true) must_be.Nil(sut) wont_be.Nil(err) } @@ -91,7 +91,23 @@ func TestCanLoadIgnoreFile(t *testing.T) { func TestCanLoadEmptyIgnoreFile(t *testing.T) { must_be, wont_be := hamlet.Specifications(t) - sut, err := pathlib.LoadIgnoreFile("testdata/empty") + sut, err := pathlib.LoadIgnoreFile("testdata/empty", true) + must_be.Nil(sut) + wont_be.Nil(err) +} + +func TestCanLoadCommentOnlyIgnoreFile(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + sut, err := pathlib.LoadIgnoreFile("testdata/commented_ignores", true) + must_be.Nil(sut) + wont_be.Nil(err) +} + +func TestCanLoadValidIgnoreFile(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + sut, err := pathlib.LoadIgnoreFile("testdata/valid_ignores", true) wont_be.Nil(sut) must_be.Nil(err) } diff --git a/robot/robot.go b/robot/robot.go index e9fdfb53..1fa18cd5 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -165,6 +165,12 @@ func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { diagnose.Fail(0, "", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } + _, err := pathlib.LoadIgnoreFile(path, true) + if err != nil { + diagnose.Warning(0, "", "Could not load ignoreFiles entry %q, reason: %v", path, err) + ok = false + continue + } } for _, path := range it.IgnoreFiles() { if !pathlib.IsFile(path) { From b050143bc49a2b7f8114c25b9a1f518268542129 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 3 Jan 2023 12:49:53 +0200 Subject: [PATCH 344/516] FEATURE: assistant artifact upload disabling (v12.1.0) - feature: on assistant runs, if CR does not give artifact URL for uploading artifacts, then it is now considered as disabled functionality (not error) and no artifacts are pushed into cloud --- cmd/assistantRun.go | 5 +++++ common/version.go | 2 +- docs/changelog.md | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 315dae2c..7b459495 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -93,6 +93,11 @@ var assistantRunCmd = &cobra.Command{ cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.assistant.run.timeline.uploaded", elapser.Elapsed().String()) }() defer func() { + if len(assistant.ArtifactURL) == 0 { + pretty.Note("Pushing artifacts to Cloud skipped (disabled, no artifact URL given).") + common.Timeline("skipping publishing artifacts (disabled, no artifact URL given)") + return + } common.Timeline("publish artifacts") publisher := operations.ArtifactPublisher{ Client: client, diff --git a/common/version.go b/common/version.go index 9eadff3b..c4c36969 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.0.1` + Version = `v12.1.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 05922b3f..6d359de3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v12.1.0 (date: 3.1.2023) + +- feature: on assistant runs, if CR does not give artifact URL for uploading + artifacts, then it is now considered as disabled functionality (not error) + and no artifacts are pushed into cloud + ## v12.0.1 (date: 3.1.2023) - added diagnostics on loading ignoreFiles entry, which does not contain From ae0a1d624a24ef5d123e8dfac3d9134c64686193 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 4 Jan 2023 08:22:52 +0200 Subject: [PATCH 345/516] BUGFIX: unzipping errors more visible (v12.1.1) - bugfix: adding more info when zip extraction fails --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/zipper.go | 28 ++++++++++++++++++---------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/common/version.go b/common/version.go index c4c36969..a14aebad 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.1.0` + Version = `v12.1.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6d359de3..e355e6b2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v12.1.1 (date: 4.1.2023) + +- bugfix: adding more info when zip extraction fails + ## v12.1.0 (date: 3.1.2023) - feature: on assistant runs, if CR does not give artifact URL for uploading diff --git a/operations/zipper.go b/operations/zipper.go index e2ca35b5..ee9726b9 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -36,27 +36,36 @@ type CommandChannel chan Command type CompletedChannel chan bool func (it *WriteTarget) Execute() bool { + err := it.execute() + if err != nil { + common.Error("zip extract", err) + common.Debug(" - failure with %q, reason: %v", it.Target, err) + } + return err == nil +} + +func (it *WriteTarget) execute() error { source, err := it.Source.Open() if err != nil { - return false + return err } defer source.Close() err = os.MkdirAll(filepath.Dir(it.Target), 0o750) if err != nil { - return false + return err } target, err := os.Create(it.Target) if err != nil { - return false + return err } defer target.Close() common.Trace("- %v", it.Target) _, err = io.Copy(target, source) if err != nil { - common.Debug(" - failure with %q, reason: %v", it.Target, err) + return err } os.Chtimes(it.Target, it.Source.Modified, it.Source.Modified) - return err == nil + return nil } type unzipper struct { @@ -161,7 +170,6 @@ func (it *unzipper) Asset(name string) ([]byte, error) { func (it *unzipper) Extract(directory string) error { common.Trace("Extracting:") - success := true for _, entry := range it.reader.File { if entry.FileInfo().IsDir() { continue @@ -171,12 +179,12 @@ func (it *unzipper) Extract(directory string) error { Source: entry, Target: target, } - success = todo.Execute() && success + err := todo.execute() + if err != nil { + return fmt.Errorf("Problem while extracting zip, reason: %v", err) + } } common.Trace("Done.") - if !success { - return fmt.Errorf("Problems while unwrapping robot. Use --debug to see details.") - } return nil } From fb825ac80d6355c5c4aa8a79a650059db7fb15a2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 11 Jan 2023 08:11:19 +0200 Subject: [PATCH 346/516] BUGFIX: parallel long path check (v12.1.2) - bugfix: parallel long path checks failed because not unique path was used, added pid as part of that long path (just Windows), this closes #45 --- common/version.go | 2 +- conda/platform_windows.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index a14aebad..40b4f984 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.1.1` + Version = `v12.1.2` ) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index c1fab164..72943b5c 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -65,7 +65,7 @@ func IsWindows() bool { } func HasLongPathSupport() bool { - baseline := []string{common.RobocorpHome(), "stump"} + baseline := []string{common.RobocorpHome(), fmt.Sprintf("stump%x", os.Getpid())} stumpath := filepath.Join(baseline...) defer os.RemoveAll(stumpath) diff --git a/docs/changelog.md b/docs/changelog.md index e355e6b2..4bb4a2f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v12.1.2 (date: 11.1.2023) + +- bugfix: parallel long path checks failed because not unique path was used, + added pid as part of that long path (just Windows), this closes #45 + ## v12.1.1 (date: 4.1.2023) - bugfix: adding more info when zip extraction fails From 034dad7957bbe4714f0cb3ceed7f9587bd7794d4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 11 Jan 2023 10:02:16 +0200 Subject: [PATCH 347/516] UPGRADE: micromamba v1.1.0 (v12.2.0) - micromamba upgrade to v1.1.0 --- common/version.go | 2 +- conda/robocorp.go | 8 ++++---- conda/robocorp_test.go | 7 +++++++ docs/changelog.md | 4 ++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 40b4f984..75821a28 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.1.2` + Version = `v12.2.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 6a20c496..d091bbd1 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - micromambaVersionLimit = 1000000 - micromambaVersionNumber = "v1.0.0" + MicromambaVersionLimit = 1_001_000 + MicromambaVersionNumber = "v1.1.0" ) var ( @@ -37,7 +37,7 @@ var ( ) func micromambaLink(platform, filename string) string { - return fmt.Sprintf("micromamba/%s/%s/%s", micromambaVersionNumber, platform, filename) + return fmt.Sprintf("micromamba/%s/%s/%s", MicromambaVersionNumber, platform, filename) } func sorted(files []os.FileInfo) { @@ -228,7 +228,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= micromambaVersionLimit + goodEnough := version >= MicromambaVersionLimit common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/conda/robocorp_test.go b/conda/robocorp_test.go index d31d2e29..f18a21c7 100644 --- a/conda/robocorp_test.go +++ b/conda/robocorp_test.go @@ -27,3 +27,10 @@ func TestCanParsePipVersion(t *testing.T) { must_be.Equal("20.3.4", second(conda.AsVersion("pip 20.3.4 from /outer/space/python/blah (python 3.9)"))) must_be.Equal("22.2.2", second(conda.AsVersion("pip 22.2.2 from /outer/space/python/blah (python 3.9)"))) } + +func TestInternalMicromambaVersionConsistency(t *testing.T) { + must_be, _ := hamlet.Specifications(t) + + needs, _ := conda.AsVersion(conda.MicromambaVersionNumber) + must_be.Equal(uint64(conda.MicromambaVersionLimit), needs) +} diff --git a/docs/changelog.md b/docs/changelog.md index 4bb4a2f8..6c2b0108 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v12.2.0 (date: 11.1.2023) + +- micromamba upgrade to v1.1.0 + ## v12.1.2 (date: 11.1.2023) - bugfix: parallel long path checks failed because not unique path was used, From edaf8c9afa9c4009d3f7ac12412554f49eff244f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 13 Jan 2023 08:43:54 +0200 Subject: [PATCH 348/516] FEATURE: remove extra middle paths in unwrap (v12.3.0) - feature: unwrap command now removes extra middle parts of file paths when unzipping robot.zip files --- cmd/assistantRun.go | 2 +- cmd/cloudPrepare.go | 2 +- cmd/communitypull.go | 2 +- cmd/pull.go | 2 +- cmd/testrun.go | 2 +- cmd/unwrap.go | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++ operations/initialize.go | 2 +- operations/pull.go | 2 +- operations/zipper.go | 66 ++++++++++++++++++++++++++++++++++------ 11 files changed, 70 insertions(+), 19 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 7b459495..f2fd5019 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -71,7 +71,7 @@ var assistantRunCmd = &cobra.Command{ defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) reason = "UNZIP_FAILURE" - err = operations.Unzip(workarea, assistant.Zipfile, false, true) + err = operations.Unzip(workarea, assistant.Zipfile, false, true, true) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index c073eccc..1ba376ac 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -43,7 +43,7 @@ var prepareCloudCmd = &cobra.Command{ pretty.Guard(err == nil, 4, "Error: %v", err) common.Debug("Using temporary workarea: %v", workarea) - err = operations.Unzip(workarea, zipfile, false, true) + err = operations.Unzip(workarea, zipfile, false, true, true) pretty.Guard(err == nil, 5, "Error: %v", err) robotfile, err := pathlib.FindNamedPath(workarea, "robot.yaml") diff --git a/cmd/communitypull.go b/cmd/communitypull.go index 6cbf280b..29e39e3a 100644 --- a/cmd/communitypull.go +++ b/cmd/communitypull.go @@ -45,7 +45,7 @@ var communityPullCmd = &cobra.Command{ pretty.Exit(1, "Download failed: %v!", err) } - err = operations.Unzip(directory, zipfile, true, false) + err = operations.Unzip(directory, zipfile, true, false, true) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/cmd/pull.go b/cmd/pull.go index c3eb7573..a2e9deb7 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -41,7 +41,7 @@ var pullCmd = &cobra.Command{ pretty.Exit(3, "Error: %v", err) } - err = operations.Unzip(directory, zipfile, forceFlag, false) + err = operations.Unzip(directory, zipfile, forceFlag, false, true) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/cmd/testrun.go b/cmd/testrun.go index c3dbdf6d..7b125ace 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -45,7 +45,7 @@ var testrunCmd = &cobra.Command{ workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) - err = operations.Unzip(workarea, zipfile, false, true) + err = operations.Unzip(workarea, zipfile, false, true, true) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/cmd/unwrap.go b/cmd/unwrap.go index 7fe396c8..941095d9 100644 --- a/cmd/unwrap.go +++ b/cmd/unwrap.go @@ -18,7 +18,7 @@ be overwritten.`, if common.DebugFlag { defer common.Stopwatch("Unwrap lasted").Report() } - err := operations.Unzip(directory, zipfile, forceFlag, false) + err := operations.Unzip(directory, zipfile, forceFlag, false, true) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/common/version.go b/common/version.go index 75821a28..25419336 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.2.0` + Version = `v12.3.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6c2b0108..237a153f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v12.3.0 (date: 13.1.2023) + +- feature: unwrap command now removes extra middle parts of file paths when + unzipping robot.zip files + ## v12.2.0 (date: 11.1.2023) - micromamba upgrade to v1.1.0 diff --git a/operations/initialize.go b/operations/initialize.go index a25ab2f8..3e66a87c 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -209,7 +209,7 @@ func templateByName(name string, internal bool) ([]byte, error) { if internal || !pathlib.IsFile(zipfile) { return blobs.Asset(blobname) } - unzipper, err := newUnzipper(zipfile) + unzipper, err := newUnzipper(zipfile, false) if err != nil { return nil, err } diff --git a/operations/pull.go b/operations/pull.go index ef14473b..cf3ed1ff 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -104,7 +104,7 @@ func ProtectedImport(filename string) (err error) { defer locker.Release() common.Timeline("Import %v", filename) - return Unzip(common.HololibLocation(), filename, true, false) + return Unzip(common.HololibLocation(), filename, true, false, false) } func PullCatalog(origin, catalogName string) (err error) { diff --git a/operations/zipper.go b/operations/zipper.go index ee9726b9..a4a87b55 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" + "github.com/robocorp/rcc/set" ) const ( @@ -69,8 +70,9 @@ func (it *WriteTarget) execute() error { } type unzipper struct { - reader *zip.Reader - closer io.Closer + reader *zip.Reader + closer io.Closer + flatten bool } func (it *unzipper) Close() { @@ -87,19 +89,21 @@ func newPayloadUnzipper(filename string) (*unzipper, error) { return nil, err } return &unzipper{ - reader: reader, - closer: payloader, + reader: reader, + closer: payloader, + flatten: false, }, nil } -func newUnzipper(filename string) (*unzipper, error) { +func newUnzipper(filename string, flatten bool) (*unzipper, error) { reader, err := zip.OpenReader(filename) if err != nil { return nil, err } return &unzipper{ - reader: &reader.Reader, - closer: reader, + reader: &reader.Reader, + closer: reader, + flatten: flatten, }, nil } @@ -168,13 +172,55 @@ func (it *unzipper) Asset(name string) ([]byte, error) { return payload, nil } +func (it *unzipper) ExtraDirectoryPrefixLength() (int, string) { + prefixes := make([]string, 0, 1) + for _, entry := range it.reader.File { + if entry.FileInfo().IsDir() { + continue + } + basename := filepath.Base(entry.Name) + if strings.ToLower(basename) != "robot.yaml" { + continue + } + dirname := filepath.Dir(entry.Name) + if len(dirname) > 0 { + prefixes = append(prefixes, dirname) + } + } + prefixes = set.Set(prefixes) + if len(prefixes) != 1 { + return 0, "" + } + prefix := prefixes[0] + if len(prefix) == 0 { + return 0, "" + } + for _, entry := range it.reader.File { + if entry.FileInfo().IsDir() { + continue + } + dirname := filepath.Dir(entry.Name) + if !strings.HasPrefix(dirname, prefix) { + return 0, "" + } + } + return len(prefix), prefix +} + func (it *unzipper) Extract(directory string) error { common.Trace("Extracting:") + limit, prefix := 0, "" + if it.flatten { + limit, prefix = it.ExtraDirectoryPrefixLength() + } + if limit > 0 { + pretty.Note("Flattening path %q out from extracted files.", prefix) + } for _, entry := range it.reader.File { if entry.FileInfo().IsDir() { continue } - target := filepath.Join(directory, slashed(entry.Name)) + target := filepath.Join(directory, slashed(entry.Name)[limit:]) todo := WriteTarget{ Source: entry, Target: target, @@ -307,7 +353,7 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { return FixDirectory(fullpath) } -func Unzip(directory, zipfile string, force, temporary bool) error { +func Unzip(directory, zipfile string, force, temporary, flatten bool) error { common.Timeline("unzip %q to %q", zipfile, directory) defer common.Timeline("unzip done") fullpath, err := filepath.Abs(directory) @@ -322,7 +368,7 @@ func Unzip(directory, zipfile string, force, temporary bool) error { if err != nil { return err } - unzip, err := newUnzipper(zipfile) + unzip, err := newUnzipper(zipfile, flatten) if err != nil { return err } From b7a4dbb4402044c0e19a3badf55640b3b4f54bfb Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 16 Jan 2023 15:37:54 +0200 Subject: [PATCH 349/516] BUGFIX: flattening failure (v12.3.1) - bugfix: unwrap worked wrongly in case of "." dir prefix --- common/version.go | 2 +- docs/changelog.md | 6 +++++- operations/zipper.go | 5 ++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 25419336..af900944 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.3.0` + Version = `v12.3.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 237a153f..65abab1e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v12.3.0 (date: 13.1.2023) +## v12.3.1 (date: 16.1.2023) + +- bugfix: unwrap worked wrongly in case of "." dir prefix + +## v12.3.0 (date: 13.1.2023) BUGGY - feature: unwrap command now removes extra middle parts of file paths when unzipping robot.zip files diff --git a/operations/zipper.go b/operations/zipper.go index a4a87b55..6e2faa80 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -183,7 +183,7 @@ func (it *unzipper) ExtraDirectoryPrefixLength() (int, string) { continue } dirname := filepath.Dir(entry.Name) - if len(dirname) > 0 { + if len(dirname) > 0 && dirname != "." { prefixes = append(prefixes, dirname) } } @@ -199,8 +199,7 @@ func (it *unzipper) ExtraDirectoryPrefixLength() (int, string) { if entry.FileInfo().IsDir() { continue } - dirname := filepath.Dir(entry.Name) - if !strings.HasPrefix(dirname, prefix) { + if !strings.HasPrefix(entry.Name, prefix) { return 0, "" } } From 67898f8f7b5e5c7f5c1b4a59fbe9fc82e68ca5d9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 17 Jan 2023 09:21:48 +0200 Subject: [PATCH 350/516] MAJOR: robot unwrap path flattening (v13.0.0) - major breaking change: various robot unzipping method now flatten directory tree so that paths used in robots are shorter and not so easily cause problems and confusion --- common/version.go | 2 +- docs/changelog.md | 10 ++++++++-- robot_tests/fullrun.robot | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index af900944..6a972bb4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v12.3.1` + Version = `v13.0.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 65abab1e..218363d0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,10 +1,16 @@ # rcc change log -## v12.3.1 (date: 16.1.2023) +## v13.0.0 (date: 17.1.2023) + +- major breaking change: various robot unzipping method now flatten directory + tree so that paths used in robots are shorter and not so easily cause + problems and confusion + +## v12.3.1 (date: 16.1.2023) MAJOR BREAK - bugfix: unwrap worked wrongly in case of "." dir prefix -## v12.3.0 (date: 13.1.2023) BUGGY +## v12.3.0 (date: 13.1.2023) BUGGY MAJOR BREAK - feature: unwrap command now removes extra middle parts of file paths when unzipping robot.zip files diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 7893bcfa..a06395fc 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v12. + Must Have v13. Goal: Show rcc license information. Step build/rcc man license --controller citests From 6f68f2f87f67757e9d03cfd161b450911f81cce9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 17 Jan 2023 12:49:11 +0200 Subject: [PATCH 351/516] BUGFIX: ignoreFiles diagnostics path issue (v13.0.1) - bugfix: diagnostics of ignoreFiles was not using paths correctly --- common/version.go | 2 +- docs/changelog.md | 4 ++++ robot/robot.go | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 6a972bb4..0fe9e3ee 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.0.0` + Version = `v13.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 218363d0..c4611578 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.0.1 (date: 17.1.2023) + +- bugfix: diagnostics of ignoreFiles was not using paths correctly + ## v13.0.0 (date: 17.1.2023) - major breaking change: various robot unzipping method now flatten directory diff --git a/robot/robot.go b/robot/robot.go index 1fa18cd5..62a83318 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -165,9 +165,11 @@ func (it *robot) diagnoseVariousPaths(diagnose common.Diagnoser) { diagnose.Fail(0, "", "ignoreFiles entry %q seems to be absolute, which makes robot machine dependent.", path) ok = false } + } + for _, path := range it.IgnoreFiles() { _, err := pathlib.LoadIgnoreFile(path, true) if err != nil { - diagnose.Warning(0, "", "Could not load ignoreFiles entry %q, reason: %v", path, err) + diagnose.Warning(0, "", "Could not load ignoreFiles entry %q, reason: %v", filepath.Base(path), err) ok = false continue } From 19a7fc40a840c4ad0e441b1b495f4963581a3f65 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 19 Jan 2023 16:55:04 +0200 Subject: [PATCH 352/516] FEATURE: configurable network diagnostics (v13.1.0) - feature: more network related configurable diagnostics --- assets/netdiag.yaml | 42 +++++++++ cmd/netdiagnostics.go | 58 +++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 + operations/diagnostics.go | 27 +++++- operations/netdiagnostics.go | 161 +++++++++++++++++++++++++++++++++++ 6 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 assets/netdiag.yaml create mode 100644 cmd/netdiagnostics.go create mode 100644 operations/netdiagnostics.go diff --git a/assets/netdiag.yaml b/assets/netdiag.yaml new file mode 100644 index 00000000..d3838931 --- /dev/null +++ b/assets/netdiag.yaml @@ -0,0 +1,42 @@ +network: + dns-lookup: + - www.robocorp.com + head-request: + - url: https://www.robocorp.com + codes: [200] + - url: https://downloads.robocorp.com/canary.txt + codes: [200] + - url: https://pypi.org/simple/jupyterlab-pygments/ + codes: [200] + - url: https://conda.anaconda.org/conda-forge/linux-64/repodata.json + codes: [200] + - url: https://cloud.robocorp.com + codes: [200] + - url: https://api.eu1.robocorp.com + codes: [403] + - url: https://api.eu1.robocloud.eu + codes: [403] + - url: https://roboworker-control-ws-v2.eu1.robocloud.eu + codes: [403] + - url: https://task-data-api.eu1.robocloud.eu + codes: [403] + - url: https://telemetry.robocorp.com + codes: [403] + - url: https://feedback.robocorp.com + codes: [403] + - url: https://status.robocorp.com + codes: [200] + - url: https://status.robocorp.com/history.atom + codes: [200] + - url: https://status.robocorp.com/history.rss + codes: [200] + - url: https://status.robocorp.com/uptime + codes: [200] + - url: https://robocorp.com/docs + codes: [200] + - url: https://robocorp.com/portal + codes: [200] + get-request: + - url: https://downloads.robocorp.com/canary.txt + codes: [200] + content-sha256: 7a8b721c71428e8599d250e647135550c05a71cc50276e9eea09d82d1baf09a1 diff --git a/cmd/netdiagnostics.go b/cmd/netdiagnostics.go new file mode 100644 index 00000000..7a6b8d08 --- /dev/null +++ b/cmd/netdiagnostics.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "os" + + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var ( + netConfigFilename string + netConfigShow bool +) + +func summonNetworkDiagConfig(filename string) ([]byte, error) { + if len(filename) == 0 { + return blobs.Asset("assets/netdiag.yaml") + } + return os.ReadFile(filename) +} + +var netDiagnosticsCmd = &cobra.Command{ + Use: "netdiagnostics", + Aliases: []string{"netdiagnostic", "netdiag"}, + Short: "Run additional diagnostics to help resolve network issues.", + Long: "Run additional diagnostics to help resolve network issues.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Netdiagnostic run lasted").Report() + } + config, err := summonNetworkDiagConfig(netConfigFilename) + if err != nil { + pretty.Exit(1, "Problem loading configuration file, reason: %v", err) + } + if netConfigShow { + common.Stdout("%s", string(config)) + os.Exit(0) + } + _, err = operations.ProduceNetDiagnostics(config, jsonFlag) + if err != nil { + pretty.Exit(1, "Error: %v", err) + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(netDiagnosticsCmd) + rootCmd.AddCommand(netDiagnosticsCmd) + + netDiagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + netDiagnosticsCmd.Flags().BoolVarP(&netConfigShow, "show", "s", false, "Show configuration instead of running diagnostics.") + netDiagnosticsCmd.Flags().StringVarP(&netConfigFilename, "config", "c", "", "Network configuration file. [optional]") +} diff --git a/common/version.go b/common/version.go index 0fe9e3ee..28deb682 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.0.1` + Version = `v13.1.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index c4611578..47ff98aa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.1.0 (date: 19.1.2023) UNSTABLE + +- feature: more network related configurable diagnostics + ## v13.0.1 (date: 17.1.2023) - bugfix: diagnostics of ignoreFiles was not using paths correctly diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 4d12acfd..15feaf68 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -433,7 +433,7 @@ func jsonDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { fmt.Fprintln(sink, form) } -func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { +func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus, showStatistics bool) { fmt.Fprintln(sink, "Diagnostics:") keys := make([]string, 0, len(details.Details)) for key, _ := range details.Details { @@ -449,6 +449,9 @@ func humaneDiagnostics(sink io.Writer, details *common.DiagnosticStatus) { for _, check := range details.Checks { fmt.Fprintf(sink, " - %-8s %-8s %s\n", check.Type, check.Status, check.Message) } + if !showStatistics { + return + } count, body := journal.MakeStatistics(12, false, false, false, false) if count > 4 { fmt.Fprintln(sink, "") @@ -469,6 +472,24 @@ func fileIt(filename string) (io.WriteCloser, error) { return file, nil } +func ProduceNetDiagnostics(body []byte, json bool) (*common.DiagnosticStatus, error) { + config, err := parseNetworkDiagnosticConfig(body) + if err != nil { + return nil, err + } + result := &common.DiagnosticStatus{ + Details: make(map[string]string), + Checks: []*common.DiagnosticCheck{}, + } + networkDiagnostics(config, result) + if json { + jsonDiagnostics(os.Stdout, result) + } else { + humaneDiagnostics(os.Stdout, result, false) + } + return nil, nil +} + func ProduceDiagnostics(filename, robotfile string, json, production bool) (*common.DiagnosticStatus, error) { file, err := fileIt(filename) if err != nil { @@ -483,7 +504,7 @@ func ProduceDiagnostics(filename, robotfile string, json, production bool) (*com if json { jsonDiagnostics(file, result) } else { - humaneDiagnostics(file, result) + humaneDiagnostics(file, result, true) } return result, nil } @@ -554,7 +575,7 @@ func PrintRobotDiagnostics(robotfile string, json, production bool) error { if json { jsonDiagnostics(os.Stdout, result) } else { - humaneDiagnostics(os.Stderr, result) + humaneDiagnostics(os.Stderr, result, true) } return nil } diff --git a/operations/netdiagnostics.go b/operations/netdiagnostics.go new file mode 100644 index 00000000..a0043bbf --- /dev/null +++ b/operations/netdiagnostics.go @@ -0,0 +1,161 @@ +package operations + +import ( + "crypto/sha256" + "fmt" + "net/url" + "strings" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/set" + "github.com/robocorp/rcc/settings" + "gopkg.in/yaml.v2" +) + +type ( + WebConfig struct { + URL string `yaml:"url"` + Codes []int `yaml:"codes"` + Fingerprint string `yaml:"content-sha256,omitempty"` + } + + NetConfig struct { + DNS []string `yaml:"dns-lookup"` + Head []*WebConfig `yaml:"head-request"` + Get []*WebConfig `yaml:"get-request"` + } + + Configuration struct { + Network *NetConfig `yaml:"network"` + } + + webtool func(string) (int, string, error) +) + +func (it *NetConfig) Hostnames() []string { + result := make([]string, 0, len(it.DNS)) + result = append(result, it.DNS...) + for _, entry := range it.Head { + parsed, err := url.Parse(entry.URL) + if err == nil { + result = append(result, parsed.Hostname()) + } + } + for _, entry := range it.Get { + parsed, err := url.Parse(entry.URL) + if err == nil { + result = append(result, parsed.Hostname()) + } + } + return set.Set(result) +} + +func parseNetworkDiagnosticConfig(body []byte) (*Configuration, error) { + config := &Configuration{} + err := yaml.Unmarshal(body, config) + if err != nil { + return nil, err + } + return config, nil +} + +func networkDiagnostics(config *Configuration, target *common.DiagnosticStatus) []*common.DiagnosticCheck { + supportUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") + if config == nil || config.Network == nil { + return target.Checks + } + diagnosticsStopwatch := common.Stopwatch("Full network diagnostics time was about") + hostnames := config.Network.Hostnames() + dnsStopwatch := common.Stopwatch("DNS lookup time for %d hostnames was about", len(hostnames)) + for _, host := range hostnames { + target.Checks = append(target.Checks, dnsLookupCheck(host)) + } + target.Details["dns-lookup-time"] = dnsStopwatch.Text() + headStopwatch := common.Stopwatch("HEAD request time for %d requests was about", len(config.Network.Head)) + for _, entry := range config.Network.Head { + target.Checks = append(target.Checks, webDiagnostics("HEAD", common.CategoryNetworkHEAD, headRequest, entry, supportUrl)...) + } + target.Details["head-time"] = headStopwatch.Text() + getStopwatch := common.Stopwatch("GET request time for %d requests was about", len(config.Network.Get)) + for _, entry := range config.Network.Get { + target.Checks = append(target.Checks, webDiagnostics("GET", common.CategoryNetworkCanary, getRequest, entry, supportUrl)...) + } + target.Details["get-time"] = getStopwatch.Text() + target.Details["diagnostics-time"] = diagnosticsStopwatch.Text() + return target.Checks +} + +func webDiagnostics(label string, category uint64, tool webtool, item *WebConfig, supportUrl string) []*common.DiagnosticCheck { + result := make([]*common.DiagnosticCheck, 0, 2) + code, fingerprint, err := tool(item.URL) + valid := set.Set(item.Codes) + member := set.Member(valid, code) + if member { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: category, + Status: statusOk, + Message: fmt.Sprintf("%s %q successful with status %d.", label, item.URL, code), + Link: supportUrl, + }) + } else { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: category, + Status: statusFail, + Message: fmt.Sprintf("%s %q failed with status %d.", label, item.URL, code), + Link: supportUrl, + }) + } + if err != nil { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: category, + Status: statusWarning, + Message: fmt.Sprintf("%s %q resulted error: %v.", label, item.URL, err), + Link: supportUrl, + }) + } + if len(item.Fingerprint) > 0 && !strings.HasPrefix(fingerprint, item.Fingerprint) { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: category, + Status: statusWarning, + Message: fmt.Sprintf("%s %q fingerprint mismatch: expected %q, but got %q instead.", label, item.URL, item.Fingerprint, fingerprint), + Link: supportUrl, + }) + } + return result +} + +func digest(body []byte) string { + digester := sha256.New() + digester.Write(body) + return fmt.Sprintf("%02x", digester.Sum([]byte{})) +} + +func headRequest(link string) (code int, fingerprint string, err error) { + defer fail.Around(&err) + + client, err := cloud.NewClient(link) + fail.On(err != nil, "Client for %q failed, reason: %v", link, err) + request := client.NewRequest("") + response := client.Head(request) + fail.On(response.Err != nil, "HEAD request to %q failed, reason: %v", link, response.Err) + + return response.Status, digest(response.Body), nil +} + +func getRequest(link string) (code int, fingerprint string, err error) { + defer fail.Around(&err) + + client, err := cloud.NewClient(link) + fail.On(err != nil, "Client for %q failed, reason: %v", link, err) + request := client.NewRequest("") + response := client.Get(request) + fail.On(response.Err != nil, "HEAD request to %q failed, reason: %v", link, response.Err) + + return response.Status, digest(response.Body), nil +} From 4e29d4ae0cd905635d3bdfb770a09d6098399b47 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 20 Jan 2023 12:02:30 +0200 Subject: [PATCH 353/516] BUGFIX: netdiagnostics flag change (v13.1.1) - fix: netdiagnostics configuration flag change (now it is `--checks filename`) --- cmd/netdiagnostics.go | 2 +- common/version.go | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/netdiagnostics.go b/cmd/netdiagnostics.go index 7a6b8d08..41698b1c 100644 --- a/cmd/netdiagnostics.go +++ b/cmd/netdiagnostics.go @@ -54,5 +54,5 @@ func init() { netDiagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") netDiagnosticsCmd.Flags().BoolVarP(&netConfigShow, "show", "s", false, "Show configuration instead of running diagnostics.") - netDiagnosticsCmd.Flags().StringVarP(&netConfigFilename, "config", "c", "", "Network configuration file. [optional]") + netDiagnosticsCmd.Flags().StringVarP(&netConfigFilename, "checks", "c", "", "Network checks configuration file. [optional]") } diff --git a/common/version.go b/common/version.go index 28deb682..732692b2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.1.0` + Version = `v13.1.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 47ff98aa..0d7acc0c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.1.1 (date: 20.1.2023) UNSTABLE + +- fix: netdiagnostics configuration flag change (now it is `--checks filename`) + ## v13.1.0 (date: 19.1.2023) UNSTABLE - feature: more network related configurable diagnostics From 8a63f79803583ae61e018a29a5583409d5333b9a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 23 Jan 2023 11:28:39 +0200 Subject: [PATCH 354/516] IMPROVEMENT: netdiagnostics trace change (v13.1.2) - improvement: netdiagnostics with `--trace` flag will now list response header information --- cloud/client.go | 20 ++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ mocks/client.go | 4 ++++ operations/netdiagnostics.go | 6 ++++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cloud/client.go b/cloud/client.go index 86236e47..3b4718b8 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -13,6 +13,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/set" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) @@ -20,6 +21,7 @@ import ( type internalClient struct { endpoint string client *http.Client + tracing bool } type Request struct { @@ -48,6 +50,7 @@ type Client interface { Delete(request *Request) *Response NewClient(endpoint string) (Client, error) WithTimeout(time.Duration) Client + WithTracing() Client } func EnsureHttps(endpoint string) (string, error) { @@ -73,6 +76,7 @@ func NewClient(endpoint string) (Client, error) { return &internalClient{ endpoint: https, client: &http.Client{Transport: settings.Global.ConfiguredHttpTransport()}, + tracing: false, }, nil } @@ -83,6 +87,15 @@ func (it *internalClient) WithTimeout(timeout time.Duration) Client { Transport: settings.Global.ConfiguredHttpTransport(), Timeout: timeout, }, + tracing: it.tracing, + } +} + +func (it *internalClient) WithTracing() Client { + return &internalClient{ + endpoint: it.endpoint, + client: it.client, + tracing: true, } } @@ -128,6 +141,13 @@ func (it *internalClient) does(method string, request *Request) *Response { return response } defer httpResponse.Body.Close() + if it.tracing { + common.Trace("Response %d headers:", httpResponse.StatusCode) + keys := set.Keys(httpResponse.Header) + for _, key := range keys { + common.Trace("> %s: %q", key, httpResponse.Header[key]) + } + } response.Status = httpResponse.StatusCode if request.Stream != nil { io.Copy(request.Stream, httpResponse.Body) diff --git a/common/version.go b/common/version.go index 732692b2..e8e5238a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.1.1` + Version = `v13.1.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0d7acc0c..e88c274f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.1.2 (date: 23.1.2023) + +- improvement: netdiagnostics with `--trace` flag will now list response + header information + ## v13.1.1 (date: 20.1.2023) UNSTABLE - fix: netdiagnostics configuration flag change (now it is `--checks filename`) diff --git a/mocks/client.go b/mocks/client.go index b1431a15..4739d26c 100644 --- a/mocks/client.go +++ b/mocks/client.go @@ -33,6 +33,10 @@ func (it *MockClient) WithTimeout(time.Duration) cloud.Client { return it } +func (it *MockClient) WithTracing() cloud.Client { + return it +} + func (it *MockClient) NewRequest(url string) *cloud.Request { return &cloud.Request{ Url: url, diff --git a/operations/netdiagnostics.go b/operations/netdiagnostics.go index a0043bbf..c73cfea4 100644 --- a/operations/netdiagnostics.go +++ b/operations/netdiagnostics.go @@ -141,6 +141,9 @@ func headRequest(link string) (code int, fingerprint string, err error) { client, err := cloud.NewClient(link) fail.On(err != nil, "Client for %q failed, reason: %v", link, err) + if common.TraceFlag { + client = client.WithTracing() + } request := client.NewRequest("") response := client.Head(request) fail.On(response.Err != nil, "HEAD request to %q failed, reason: %v", link, response.Err) @@ -153,6 +156,9 @@ func getRequest(link string) (code int, fingerprint string, err error) { client, err := cloud.NewClient(link) fail.On(err != nil, "Client for %q failed, reason: %v", link, err) + if common.TraceFlag { + client = client.WithTracing() + } request := client.NewRequest("") response := client.Get(request) fail.On(response.Err != nil, "HEAD request to %q failed, reason: %v", link, response.Err) From 6dd10a7e52e78cb096ee8bed0b16c15a25d97209 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Jan 2023 12:47:23 +0200 Subject: [PATCH 355/516] FEATURE: remote peercc pull (v13.2.0) - feature: peercc force pulling holotree catalog from other remote peercc - self pulling should be prevented and so protect self loops - new settings version, 2023.01 with autoupdates for lab removed and setup-utility added --- assets/settings.yaml | 4 ++-- cmd/holotreePull.go | 7 ++++-- common/variables.go | 11 +++++++++- common/version.go | 2 +- docs/changelog.md | 7 ++++++ operations/pull.go | 9 ++++++-- peercc/delta.go | 10 +++++++++ peercc/listings.go | 8 ++++++- peercc/server.go | 8 +++++-- peercc/trigger.go | 51 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 peercc/trigger.go diff --git a/assets/settings.yaml b/assets/settings.yaml index a53971e8..9e399623 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -20,7 +20,7 @@ autoupdates: assistant: https://downloads.robocorp.com/assistant/releases/ automation-studio: https://downloads.robocorp.com/automation-studio/releases/ workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ - lab: https://downloads.robocorp.com/lab/releases/ + setup-utility: https://downloads.robocorp.com/setup-utility/releases/ templates: https://downloads.robocorp.com/templates/templates.yaml certificates: @@ -42,4 +42,4 @@ meta: name: default description: default settings.yaml internal to rcc source: builtin - version: 2022.09 + version: 2023.01 diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index 58f50e0c..f47a806b 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -39,9 +39,12 @@ var holotreePullCmd = &cobra.Command{ } func init() { + remoteOrigin := common.RccRemoteOrigin() holotreeCmd.AddCommand(holotreePullCmd) holotreePullCmd.Flags().BoolVarP(&forcePull, "force", "", false, "Force pull check, even when blueprint is already present.") - holotreePullCmd.Flags().StringVarP(&remoteOrigin, "origin", "o", "", "URL of remote origin to pull environment from.") + holotreePullCmd.Flags().StringVarP(&remoteOrigin, "origin", "o", remoteOrigin, "URL of remote origin to pull environment from.") holotreePullCmd.Flags().StringVarP(&pullRobot, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file to export as catalog. ") - holotreePullCmd.MarkFlagRequired("origin") + if len(remoteOrigin) == 0 { + holotreePullCmd.MarkFlagRequired("origin") + } } diff --git a/common/variables.go b/common/variables.go index 6019e730..1a2d9a9b 100644 --- a/common/variables.go +++ b/common/variables.go @@ -12,6 +12,7 @@ import ( const ( ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` + RCC_REMOTE_ORIGIN = `RCC_REMOTE_ORIGIN` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` ) @@ -64,6 +65,10 @@ func init() { ensureDirectory(MambaPackages()) } +func RandomIdentifier() string { + return randomIdentifier +} + func RobocorpHome() string { if len(ForcedRobocorpHome) > 0 { return ExpandPath(ForcedRobocorpHome) @@ -75,6 +80,10 @@ func RobocorpHome() string { return ExpandPath(defaultRobocorpLocation) } +func RccRemoteOrigin() string { + return os.Getenv(RCC_REMOTE_ORIGIN) +} + func RobocorpLock() string { return filepath.Join(RobocorpHome(), "robocorp.lck") } @@ -116,7 +125,7 @@ func RobocorpTempRoot() string { } func RobocorpTempName() string { - return filepath.Join(RobocorpTempRoot(), randomIdentifier) + return filepath.Join(RobocorpTempRoot(), RandomIdentifier()) } func RobocorpTemp() string { diff --git a/common/version.go b/common/version.go index e8e5238a..16a14980 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.1.2` + Version = `v13.2.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index e88c274f..f4f2b258 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v13.2.0 (date: 24.1.2023) + +- feature: peercc force pulling holotree catalog from other remote peercc +- self pulling should be prevented and so protect self loops +- new settings version, 2023.01 with autoupdates for lab removed and + setup-utility added + ## v13.1.2 (date: 23.1.2023) - improvement: netdiagnostics with `--trace` flag will now list response diff --git a/operations/pull.go b/operations/pull.go index cf3ed1ff..5050e98c 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -16,11 +16,14 @@ import ( "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/xviper" ) +const ( + X_RCC_RANDOM_IDENTITY = `X-Rcc-Random-Identity` +) + func pullOriginFingerprints(origin, catalogName string) (fingerprints string, count int, err error) { defer fail.Around(&err) @@ -28,8 +31,9 @@ func pullOriginFingerprints(origin, catalogName string) (fingerprints string, co fail.On(err != nil, "Could not create web client for %q, reason: %v", origin, err) request := client.NewRequest(fmt.Sprintf("/parts/%s", catalogName)) + request.Headers[X_RCC_RANDOM_IDENTITY] = common.RandomIdentifier() response := client.Get(request) - pretty.Guard(response.Status == 200, 5, "Problem with parts request, status=%d, body=%s", response.Status, response.Body) + fail.On(response.Status != 200, "Problem with parts request, status=%d, body=%s", response.Status, response.Body) stream := bufio.NewReader(bytes.NewReader(response.Body)) collection := make([]string, 0, 2048) @@ -65,6 +69,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil request.Header.Add("robocorp-installation-id", xviper.TrackingIdentity()) request.Header.Add("User-Agent", common.UserAgent()) + request.Header.Add(X_RCC_RANDOM_IDENTITY, common.RandomIdentifier()) response, err := client.Do(request) fail.On(err != nil, "Web request to %q failed, reason: %v", url, err) diff --git a/peercc/delta.go b/peercc/delta.go index c0ed41ac..f88cf6fe 100644 --- a/peercc/delta.go +++ b/peercc/delta.go @@ -14,6 +14,11 @@ import ( "github.com/robocorp/rcc/set" ) +func isSelfRequest(request *http.Request) bool { + identity, ok := request.Header[operations.X_RCC_RANDOM_IDENTITY] + return ok && len(identity) > 0 && identity[0] == common.RandomIdentifier() +} + func makeDeltaHandler(queries Partqueries) http.HandlerFunc { return func(response http.ResponseWriter, request *http.Request) { catalog := filepath.Base(request.URL.Path) @@ -23,6 +28,11 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { common.Trace("Delta: rejecting request %q for catalog %q.", request.Method, catalog) return } + if isSelfRequest(request) { + response.WriteHeader(http.StatusConflict) + common.Trace("Delta: rejecting /SELF/ request for catalog %q.", catalog) + return + } reply := make(chan string) queries <- &Partquery{ Catalog: catalog, diff --git a/peercc/listings.go b/peercc/listings.go index 94cef56f..c0660094 100644 --- a/peercc/listings.go +++ b/peercc/listings.go @@ -16,7 +16,7 @@ const ( partCacheSize = 20 ) -func makeQueryHandler(queries Partqueries) http.HandlerFunc { +func makeQueryHandler(queries Partqueries, triggers chan string) http.HandlerFunc { return func(response http.ResponseWriter, request *http.Request) { catalog := filepath.Base(request.URL.Path) defer common.Stopwatch("Query of catalog %q took", catalog).Debug() @@ -25,6 +25,11 @@ func makeQueryHandler(queries Partqueries) http.HandlerFunc { common.Trace("Query: rejecting request %q for catalog %q.", request.Method, catalog) return } + if isSelfRequest(request) { + response.WriteHeader(http.StatusConflict) + common.Trace("Query: rejecting /SELF/ request for catalog %q.", catalog) + return + } reply := make(chan string) queries <- &Partquery{ Catalog: catalog, @@ -33,6 +38,7 @@ func makeQueryHandler(queries Partqueries) http.HandlerFunc { content, ok := <-reply common.Debug("query handler: %q -> %v", catalog, ok) if !ok { + triggers <- catalog response.WriteHeader(http.StatusNotFound) response.Write([]byte("404 not found, sorry")) return diff --git a/peercc/server.go b/peercc/server.go index 23299792..75ef63e9 100644 --- a/peercc/server.go +++ b/peercc/server.go @@ -23,11 +23,14 @@ func Serve(address string, port int, domain, storage string) error { } defer cleanupHoldStorage(holding) - partqueries := make(Partqueries) + triggers := make(chan string, 20) + defer close(triggers) + partqueries := make(Partqueries) defer close(partqueries) go listProvider(partqueries) + go pullProcess(triggers) listen := fmt.Sprintf("%s:%d", address, port) mux := http.NewServeMux() @@ -39,8 +42,9 @@ func Serve(address string, port int, domain, storage string) error { MaxHeaderBytes: 1 << 14, } - mux.HandleFunc("/parts/", makeQueryHandler(partqueries)) + mux.HandleFunc("/parts/", makeQueryHandler(partqueries, triggers)) mux.HandleFunc("/delta/", makeDeltaHandler(partqueries)) + mux.HandleFunc("/force/", makeTriggerHandler(triggers)) go server.ListenAndServe() diff --git a/peercc/trigger.go b/peercc/trigger.go new file mode 100644 index 00000000..955e8616 --- /dev/null +++ b/peercc/trigger.go @@ -0,0 +1,51 @@ +package peercc + +import ( + "net/http" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" +) + +func makeTriggerHandler(requests chan string) http.HandlerFunc { + return func(response http.ResponseWriter, request *http.Request) { + catalog := filepath.Base(request.URL.Path) + defer common.Stopwatch("Trigger of catalog %q took", catalog).Debug() + requests <- catalog + } +} + +func pullOperation(counter int, catalog, remoteOrigin string) { + defer common.Stopwatch("#%d: pull opearation lasted", counter).Report() + common.Log("#%d: Trying to pull %q from %q ...", counter, catalog, remoteOrigin) + err := operations.PullCatalog(remoteOrigin, catalog) + if err != nil { + pretty.Warning("#%d: Failed to pull %q from %q, reason: %v", counter, catalog, remoteOrigin, err) + } else { + common.Log("#%d: Pull %q from %q completed.", counter, catalog, remoteOrigin) + } +} + +func pullProcess(requests chan string) { + remoteOrigin := common.RccRemoteOrigin() + disabled := len(remoteOrigin) == 0 + if disabled { + pretty.Note("Wont pull anything since RCC_REMOTE_ORIGIN is not defined.") + } + counter := 0 +forever: + for { + catalog, ok := <-requests + if !ok { + break forever + } + counter += 1 + if disabled { + pretty.Warning("Cannot #%d pull %q since RCC_REMOTE_ORIGIN is not defined.", counter, catalog) + continue + } + pullOperation(counter, catalog, remoteOrigin) + } +} From a13e2f8d8db4cb5f4527e8bdb66c1800362edbf2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 27 Jan 2023 12:05:42 +0200 Subject: [PATCH 356/516] FEATURE: command to prebuild environments (v13.3.0) - feature: command for prebuilding environments (from files or from URLs) - improvement: rcc version visible in "Toplevel" command list - added support for "cloud.ReadFile" functionality - bugfix: wrapped os.TempDir functionality to ensure directory exists --- cloud/readfile.go | 28 +++++++++++ cmd/assistantRun.go | 2 +- cmd/carrier.go | 2 +- cmd/cloudPrepare.go | 4 +- cmd/communitypull.go | 3 +- cmd/holotreeBootstrap.go | 3 +- cmd/holotreeImport.go | 3 +- cmd/holotreePrebuild.go | 100 +++++++++++++++++++++++++++++++++++++++ cmd/peercc/main.go | 2 +- cmd/pull.go | 3 +- cmd/push.go | 3 +- cmd/root.go | 2 +- cmd/testrun.go | 4 +- common/version.go | 2 +- conda/condayaml.go | 3 +- conda/workflows.go | 4 +- docs/changelog.md | 7 +++ operations/pull.go | 4 +- operations/updownload.go | 3 +- pathlib/functions.go | 10 ++++ 20 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 cloud/readfile.go create mode 100644 cmd/holotreePrebuild.go diff --git a/cloud/readfile.go b/cloud/readfile.go new file mode 100644 index 00000000..3ae6f390 --- /dev/null +++ b/cloud/readfile.go @@ -0,0 +1,28 @@ +package cloud + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" +) + +func ReadFile(resource string) ([]byte, error) { + link, err := url.ParseRequestURI(resource) + if err != nil { + return os.ReadFile(resource) + } + if link.Scheme == "file" || link.Scheme == "" { + return os.ReadFile(link.Path) + } + tempfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("temp%x.part", common.When)) + defer os.Remove(tempfile) + err = Download(resource, tempfile) + if err != nil { + return nil, err + } + return os.ReadFile(tempfile) +} diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index f2fd5019..754255ee 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -67,7 +67,7 @@ var assistantRunCmd = &cobra.Command{ common.Debug("Robot Assistant run-id is %v.", assistant.RunId) common.Debug("With task '%v' from zip %v.", assistant.TaskName, assistant.Zipfile) sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) + workarea := filepath.Join(pathlib.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) reason = "UNZIP_FAILURE" diff --git a/cmd/carrier.go b/cmd/carrier.go index 5a0db575..452bd133 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -48,7 +48,7 @@ func runCarrier() error { return err } sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) + workarea := filepath.Join(pathlib.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) carrier, err := operations.FindExecutable() diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 1ba376ac..dc992319 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -27,10 +27,10 @@ var prepareCloudCmd = &cobra.Command{ defer common.Stopwatch("Cloud prepare lasted").Report() } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) defer os.Remove(zipfile) - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) + workarea := filepath.Join(pathlib.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) account := operations.AccountByName(AccountName()) diff --git a/cmd/communitypull.go b/cmd/communitypull.go index 29e39e3a..e9b0869f 100644 --- a/cmd/communitypull.go +++ b/cmd/communitypull.go @@ -7,6 +7,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -26,7 +27,7 @@ var communityPullCmd = &cobra.Command{ defer common.Stopwatch("Pull lasted").Report() } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 497953da..90deeba2 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" "github.com/spf13/cobra" @@ -21,7 +22,7 @@ func updateEnvironments(robots []string) { tree, err := htfs.New() pretty.Guard(err == nil, 2, "Holotree creation error: %v", err) for at, template := range robots { - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) + workarea := filepath.Join(pathlib.TempDir(), fmt.Sprintf("workarea%x%x", common.When, at)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) err = operations.InitializeWorkarea(workarea, template, false, forceFlag) diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go index 76f8e634..2f8f350c 100644 --- a/cmd/holotreeImport.go +++ b/cmd/holotreeImport.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) @@ -23,7 +24,7 @@ func isUrl(name string) bool { func temporaryDownload(at int, link string) (string, error) { common.Timeline("Download %v", link) - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("hololib%x%x.zip", common.When, at)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("hololib%x%x.zip", common.When, at)) err := cloud.Download(link, zipfile) if err != nil { return "", err diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go new file mode 100644 index 00000000..df08cd8b --- /dev/null +++ b/cmd/holotreePrebuild.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "net/url" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + metafileFlag bool +) + +func conditionalExpand(filename string) string { + if !pathlib.IsFile(filename) { + return filename + } + fullpath, err := filepath.Abs(filename) + if err != nil { + return filename + } + return fullpath +} + +func resolveMetafile(link string) ([]string, error) { + origin, err := url.Parse(link) + refok := err == nil + raw, err := cloud.ReadFile(link) + if err != nil { + return nil, err + } + result := []string{} + for _, line := range strings.SplitAfter(string(raw), "\n") { + flat := strings.TrimSpace(line) + if strings.HasPrefix(flat, "#") || len(flat) == 0 { + continue + } + here, err := url.Parse(flat) + if refok && err == nil { + relative := origin.ResolveReference(here) + result = append(result, relative.String()) + } else { + result = append(result, flat) + } + } + return result, nil +} + +func metafileExpansion(links []string, expand bool) []string { + if !expand { + return links + } + result := []string{} + for _, metalink := range links { + links, err := resolveMetafile(conditionalExpand(metalink)) + if err != nil { + pretty.Warning("Failed to resolve %q metafile, reason: %v", metalink, err) + continue + } + result = append(result, links...) + } + return result +} + +var holotreePrebuildCmd = &cobra.Command{ + Use: "prebuild", + Short: "Prebuild hololib from given set of environment descriptors.", + Long: "Prebuild hololib from given set of environment descriptors.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Holotree prebuild lasted").Report() + } + + total, failed := 0, 0 + + for _, configfile := range metafileExpansion(args, metafileFlag) { + total += 1 + pretty.Note("Now building config %q", configfile) + _, _, err := htfs.NewEnvironment(configfile, "", false, false) + if err != nil { + failed += 1 + pretty.Warning("Holotree recording error: %v", err) + } + } + pretty.Guard(failed == 0, 1, "%d out of %d environment builds failed! See output above for details.", failed, total) + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreePrebuildCmd) + holotreePrebuildCmd.Flags().BoolVarP(&metafileFlag, "metafile", "m", false, "Input arguments are actually files containing links/filenames of environment descriptors.") +} diff --git a/cmd/peercc/main.go b/cmd/peercc/main.go index 078b4566..7eb72438 100644 --- a/cmd/peercc/main.go +++ b/cmd/peercc/main.go @@ -20,7 +20,7 @@ var ( ) func defaultHoldLocation() string { - where, err := pathlib.Abs(filepath.Join(os.TempDir(), "peercchold")) + where, err := pathlib.Abs(filepath.Join(pathlib.TempDir(), "peercchold")) if err != nil { return "temphold" } diff --git a/cmd/pull.go b/cmd/pull.go index a2e9deb7..0f1c7f8e 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -32,7 +33,7 @@ var pullCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v reason %v", account.Endpoint, err) } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("pull%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/push.go b/cmd/push.go index 2544aa32..c084213a 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -30,7 +31,7 @@ var pushCmd = &cobra.Command{ pretty.Exit(2, "Could not create client for endpoint: %v reason: %v", account.Endpoint, err) } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("push%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("push%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) diff --git a/cmd/root.go b/cmd/root.go index d19df1cf..97885d37 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,7 +24,7 @@ var ( ) func toplevelCommands(parent *cobra.Command) { - common.Log("\nToplevel commands") + common.Log("\nToplevel commands (%v)", common.Version) for _, child := range parent.Commands() { if child.Hidden || len(child.Commands()) > 0 { continue diff --git a/cmd/testrun.go b/cmd/testrun.go index 7b125ace..b415b463 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -28,7 +28,7 @@ var testrunCmd = &cobra.Command{ defer common.Stopwatch("Task testrun lasted").Report() } now := time.Now() - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("testrun%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("testrun%x.zip", common.When)) defer os.Remove(zipfile) common.Debug("Using temporary zip file: %v", zipfile) sourceDir := filepath.Dir(robotFile) @@ -42,7 +42,7 @@ var testrunCmd = &cobra.Command{ pretty.Exit(2, "Error: %v", err) } sentinelTime := time.Now() - workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) + workarea := filepath.Join(pathlib.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) common.Debug("Using temporary workarea: %v", workarea) err = operations.Unzip(workarea, zipfile, false, true, true) diff --git a/common/version.go b/common/version.go index 16a14980..f6ee6ea7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.2.0` + Version = `v13.3.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index b8d2a0b7..5387a82c 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -6,6 +6,7 @@ import ( "regexp" "strings" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -550,7 +551,7 @@ func CondaYamlFrom(content []byte) (*Environment, error) { } func ReadCondaYaml(filename string) (*Environment, error) { - content, err := os.ReadFile(filename) + content, err := cloud.ReadFile(filename) if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } diff --git a/conda/workflows.go b/conda/workflows.go index 76a04865..9a9bcd7b 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -349,8 +349,8 @@ func LegacyEnvironment(force bool, configurations ...string) error { freshInstall := true - condaYaml := filepath.Join(os.TempDir(), fmt.Sprintf("conda_%x.yaml", common.When)) - requirementsText := filepath.Join(os.TempDir(), fmt.Sprintf("require_%x.txt", common.When)) + condaYaml := filepath.Join(pathlib.TempDir(), fmt.Sprintf("conda_%x.yaml", common.When)) + requirementsText := filepath.Join(pathlib.TempDir(), fmt.Sprintf("require_%x.txt", common.When)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) if err != nil { diff --git a/docs/changelog.md b/docs/changelog.md index f4f2b258..40651320 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v13.3.0 (date: 27.1.2023) + +- feature: command for prebuilding environments (from files or from URLs) +- improvement: rcc version visible in "Toplevel" command list +- added support for "cloud.ReadFile" functionality +- bugfix: wrapped os.TempDir functionality to ensure directory exists + ## v13.2.0 (date: 24.1.2023) - feature: peercc force pulling holotree catalog from other remote peercc diff --git a/operations/pull.go b/operations/pull.go index 5050e98c..ac057c4c 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -61,7 +61,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil url := fmt.Sprintf("%s/delta/%s", origin, catalogName) body := strings.NewReader(selection) - filename = filepath.Join(os.TempDir(), fmt.Sprintf("peercc_%x.zip", os.Getpid())) + filename = filepath.Join(pathlib.TempDir(), fmt.Sprintf("peercc_%x.zip", os.Getpid())) client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} request, err := http.NewRequest("POST", url, body) @@ -91,7 +91,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil fail.On(err != nil, "Download failed, reason: %v", err) sum := fmt.Sprintf("%02x", digest.Sum(nil)) - finalname := filepath.Join(os.TempDir(), fmt.Sprintf("peercc_%s.zip", sum)) + finalname := filepath.Join(pathlib.TempDir(), fmt.Sprintf("peercc_%s.zip", sum)) err = pathlib.TryRename("delta", filename, finalname) fail.On(err != nil, "Rename %q -> %q failed, reason: %v", filename, finalname, err) diff --git a/operations/updownload.go b/operations/updownload.go index 52aa6e81..d874b9e7 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" ) const ( @@ -168,7 +169,7 @@ func SummonRobotZipfile(client cloud.Client, account *account, workspaceId, robo if ok { return found, nil } - zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) + zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) err := DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) if err != nil { return "", err diff --git a/pathlib/functions.go b/pathlib/functions.go index 056a072c..ca9d3892 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -10,8 +10,18 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pretty" ) +func TempDir() string { + base := os.TempDir() + _, err := EnsureDirectory(base) + if err != nil { + pretty.Warning("TempDir %q challenge, reason: %v", base, err) + } + return base +} + func Exists(pathname string) bool { _, err := os.Stat(pathname) return !os.IsNotExist(err) From 65186db548d9020c3f51de97de8f24794bc82561 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Jan 2023 09:29:50 +0200 Subject: [PATCH 357/516] RENAME: peercc to rccremote (v13.4.0) - peercc is renamed to rccremote, and peercc package renamed to remotree --- README.md | 2 +- cmd/{peercc => rccremote}/main.go | 12 ++++++------ cmd/{peercc => rccremote}/main_test.go | 0 common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/pull.go | 4 ++-- peercc/missing_test.go | 1 - {peercc => remotree}/delta.go | 2 +- {peercc => remotree}/listings.go | 4 ++-- {peercc => remotree}/manage.go | 2 +- {peercc => remotree}/messages.go | 2 +- remotree/missing_test.go | 1 + {peercc => remotree}/server.go | 2 +- {peercc => remotree}/trigger.go | 2 +- 14 files changed, 22 insertions(+), 18 deletions(-) rename cmd/{peercc => rccremote}/main.go (79%) rename cmd/{peercc => rccremote}/main_test.go (100%) delete mode 100644 peercc/missing_test.go rename {peercc => remotree}/delta.go (99%) rename {peercc => remotree}/listings.go (97%) rename {peercc => remotree}/manage.go (96%) rename {peercc => remotree}/messages.go (85%) create mode 100644 remotree/missing_test.go rename {peercc => remotree}/server.go (98%) rename {peercc => remotree}/trigger.go (98%) diff --git a/README.md b/README.md index f64da4aa..c8fc8c82 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Changelog can be seen [here.](/docs/changelog.md) It is also visible inside rcc Some tips, tricks, and recipes can be found [here.](/docs/recipes.md) They are also visible inside rcc using command `rcc docs recipes`. -## Community +## Community and Support The Robocorp community can be found on [Developer Slack](https://robocorp-developers.slack.com), where you can ask questions, voice ideas, and share your projects. diff --git a/cmd/peercc/main.go b/cmd/rccremote/main.go similarity index 79% rename from cmd/peercc/main.go rename to cmd/rccremote/main.go index 7eb72438..933cc4fc 100644 --- a/cmd/peercc/main.go +++ b/cmd/rccremote/main.go @@ -7,8 +7,8 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/peercc" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/remotree" ) var ( @@ -20,7 +20,7 @@ var ( ) func defaultHoldLocation() string { - where, err := pathlib.Abs(filepath.Join(pathlib.TempDir(), "peercchold")) + where, err := pathlib.Abs(filepath.Join(pathlib.TempDir(), "rccremotehold")) if err != nil { return "temphold" } @@ -31,7 +31,7 @@ func init() { flag.BoolVar(&common.DebugFlag, "debug", false, "Turn on debugging output.") flag.BoolVar(&common.TraceFlag, "trace", false, "Turn on tracing output.") - flag.BoolVar(&versionFlag, "version", false, "Just show peercc version and exit.") + flag.BoolVar(&versionFlag, "version", false, "Just show rccremote version and exit.") flag.StringVar(&serverName, "hostname", "localhost", "Hostname/address to bind server to.") flag.IntVar(&serverPort, "port", 4653, "Port to bind server in given hostname.") flag.StringVar(&holdingArea, "hold", defaultHoldLocation(), "Directory where to put HOLD files once known.") @@ -62,9 +62,9 @@ func process() { if versionFlag { showVersion() } - pretty.Guard(common.SharedHolotree, 1, "Shared holotree must be enabled and in use for peercc to work.") - common.Log("Peer for rcc starting (%s) ...", common.Version) - peercc.Serve(serverName, serverPort, domainId, holdingArea) + pretty.Guard(common.SharedHolotree, 1, "Shared holotree must be enabled and in use for rccremote to work.") + common.Log("Remote for rcc starting (%s) ...", common.Version) + remotree.Serve(serverName, serverPort, domainId, holdingArea) } func main() { diff --git a/cmd/peercc/main_test.go b/cmd/rccremote/main_test.go similarity index 100% rename from cmd/peercc/main_test.go rename to cmd/rccremote/main_test.go diff --git a/common/version.go b/common/version.go index f6ee6ea7..22c88872 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.3.0` + Version = `v13.4.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 40651320..788cf876 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.4.0 (date: 30.1.2023) + +- peercc is renamed to rccremote, and peercc package renamed to remotree + ## v13.3.0 (date: 27.1.2023) - feature: command for prebuilding environments (from files or from URLs) diff --git a/operations/pull.go b/operations/pull.go index ac057c4c..aeadd5cd 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -61,7 +61,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil url := fmt.Sprintf("%s/delta/%s", origin, catalogName) body := strings.NewReader(selection) - filename = filepath.Join(pathlib.TempDir(), fmt.Sprintf("peercc_%x.zip", os.Getpid())) + filename = filepath.Join(pathlib.TempDir(), fmt.Sprintf("rccremote_%x.zip", os.Getpid())) client := &http.Client{Transport: settings.Global.ConfiguredHttpTransport()} request, err := http.NewRequest("POST", url, body) @@ -91,7 +91,7 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil fail.On(err != nil, "Download failed, reason: %v", err) sum := fmt.Sprintf("%02x", digest.Sum(nil)) - finalname := filepath.Join(pathlib.TempDir(), fmt.Sprintf("peercc_%s.zip", sum)) + finalname := filepath.Join(pathlib.TempDir(), fmt.Sprintf("rccremote_%s.zip", sum)) err = pathlib.TryRename("delta", filename, finalname) fail.On(err != nil, "Rename %q -> %q failed, reason: %v", filename, finalname, err) diff --git a/peercc/missing_test.go b/peercc/missing_test.go deleted file mode 100644 index fa32ef9f..00000000 --- a/peercc/missing_test.go +++ /dev/null @@ -1 +0,0 @@ -package peercc_test diff --git a/peercc/delta.go b/remotree/delta.go similarity index 99% rename from peercc/delta.go rename to remotree/delta.go index f88cf6fe..a7263356 100644 --- a/peercc/delta.go +++ b/remotree/delta.go @@ -1,4 +1,4 @@ -package peercc +package remotree import ( "archive/zip" diff --git a/peercc/listings.go b/remotree/listings.go similarity index 97% rename from peercc/listings.go rename to remotree/listings.go index c0660094..dde911c5 100644 --- a/peercc/listings.go +++ b/remotree/listings.go @@ -1,4 +1,4 @@ -package peercc +package remotree import ( "bufio" @@ -54,7 +54,7 @@ func makeQueryHandler(queries Partqueries, triggers chan string) http.HandlerFun func loadSingleCatalog(catalog string) (root *htfs.Root, err error) { defer fail.Around(&err) - tempdir := filepath.Join(common.RobocorpTemp(), "peercc") + tempdir := filepath.Join(common.RobocorpTemp(), "rccremote") shadow, err := htfs.NewRoot(tempdir) fail.On(err != nil, "Could not create root, reason: %v", err) filename := filepath.Join(common.HololibCatalogLocation(), catalog) diff --git a/peercc/manage.go b/remotree/manage.go similarity index 96% rename from peercc/manage.go rename to remotree/manage.go index 5963d656..8764904c 100644 --- a/peercc/manage.go +++ b/remotree/manage.go @@ -1,4 +1,4 @@ -package peercc +package remotree import ( "path/filepath" diff --git a/peercc/messages.go b/remotree/messages.go similarity index 85% rename from peercc/messages.go rename to remotree/messages.go index 6449fe4c..de32f1cb 100644 --- a/peercc/messages.go +++ b/remotree/messages.go @@ -1,4 +1,4 @@ -package peercc +package remotree type ( Partquery struct { diff --git a/remotree/missing_test.go b/remotree/missing_test.go new file mode 100644 index 00000000..3410dd67 --- /dev/null +++ b/remotree/missing_test.go @@ -0,0 +1 @@ +package remotree_test diff --git a/peercc/server.go b/remotree/server.go similarity index 98% rename from peercc/server.go rename to remotree/server.go index 75ef63e9..06102e1b 100644 --- a/peercc/server.go +++ b/remotree/server.go @@ -1,4 +1,4 @@ -package peercc +package remotree import ( "context" diff --git a/peercc/trigger.go b/remotree/trigger.go similarity index 98% rename from peercc/trigger.go rename to remotree/trigger.go index 955e8616..2d2c3bf6 100644 --- a/peercc/trigger.go +++ b/remotree/trigger.go @@ -1,4 +1,4 @@ -package peercc +package remotree import ( "net/http" From e8d49f2f71455edd42b4a92733bf9ee22c427d90 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Jan 2023 10:52:39 +0200 Subject: [PATCH 358/516] IMPROVEMENT: prebuild improvements (v13.4.1) - prebuild now needs shared holotree to be enabled before building - prebuilds can now be forced for full rebuilds --- cmd/holotreePrebuild.go | 19 +++++++++++-------- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index df08cd8b..427feed8 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -15,6 +15,7 @@ import ( var ( metafileFlag bool + forceBuild bool ) func conditionalExpand(filename string) string { @@ -71,25 +72,26 @@ func metafileExpansion(links []string, expand bool) []string { var holotreePrebuildCmd = &cobra.Command{ Use: "prebuild", Short: "Prebuild hololib from given set of environment descriptors.", - Long: "Prebuild hololib from given set of environment descriptors.", + Long: "Prebuild hololib from given set of environment descriptors. Requires shared holotree to be enabled and active.", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag { defer common.Stopwatch("Holotree prebuild lasted").Report() } - total, failed := 0, 0 + pretty.Guard(common.SharedHolotree, 1, "Shared holotree must be enabled and in use for prebuild environments to work correctly.") - for _, configfile := range metafileExpansion(args, metafileFlag) { - total += 1 - pretty.Note("Now building config %q", configfile) - _, _, err := htfs.NewEnvironment(configfile, "", false, false) + configurations := metafileExpansion(args, metafileFlag) + total, failed := len(configurations), 0 + for at, configfile := range configurations { + pretty.Note("%d/%d: Now building config %q", at+1, total, configfile) + _, _, err := htfs.NewEnvironment(configfile, "", false, forceBuild) if err != nil { failed += 1 - pretty.Warning("Holotree recording error: %v", err) + pretty.Warning("%d/%d: Holotree recording error: %v", at+1, total, err) } } - pretty.Guard(failed == 0, 1, "%d out of %d environment builds failed! See output above for details.", failed, total) + pretty.Guard(failed == 0, 2, "%d out of %d environment builds failed! See output above for details.", failed, total) pretty.Ok() }, } @@ -97,4 +99,5 @@ var holotreePrebuildCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreePrebuildCmd) holotreePrebuildCmd.Flags().BoolVarP(&metafileFlag, "metafile", "m", false, "Input arguments are actually files containing links/filenames of environment descriptors.") + holotreePrebuildCmd.Flags().BoolVarP(&forceBuild, "force", "f", false, "Force environment builds, even when blueprint is already present.") } diff --git a/common/version.go b/common/version.go index 22c88872..3f8b0d91 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.4.0` + Version = `v13.4.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 788cf876..3f590701 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.4.1 (date: 30.1.2023) + +- prebuild now needs shared holotree to be enabled before building +- prebuilds can now be forced for full rebuilds + ## v13.4.0 (date: 30.1.2023) - peercc is renamed to rccremote, and peercc package renamed to remotree From 75346838a2529f81f2fc32032756e9e4e4e19705 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 31 Jan 2023 13:50:05 +0200 Subject: [PATCH 359/516] BUGFIX: holotree pull fix (v13.4.2) - fixed broken holotree pull command, and made it allow pulling from plain http sources --- cloud/client.go | 8 ++++++++ cmd/holotreePull.go | 14 +++++++------- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/pull.go | 2 +- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index 3b4718b8..c0ad44b2 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -68,6 +68,14 @@ func EnsureHttps(endpoint string) (string, error) { return nice, nil } +func NewUnsafeClient(endpoint string) (Client, error) { + return &internalClient{ + endpoint: endpoint, + client: &http.Client{Transport: settings.Global.ConfiguredHttpTransport()}, + tracing: false, + }, nil +} + func NewClient(endpoint string) (Client, error) { https, err := EnsureHttps(endpoint) if err != nil { diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index f47a806b..8a00a21a 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -9,9 +9,9 @@ import ( ) var ( - remoteOrigin string - pullRobot string - forcePull bool + remoteOriginOption string + pullRobot string + forcePull bool ) var holotreePullCmd = &cobra.Command{ @@ -31,7 +31,7 @@ var holotreePullCmd = &cobra.Command{ present := tree.HasBlueprint(holotreeBlueprint) if !present || forcePull { catalog := htfs.CatalogName(hash) - err = operations.PullCatalog(remoteOrigin, catalog) + err = operations.PullCatalog(remoteOriginOption, catalog) pretty.Guard(err == nil, 3, "%s", err) } pretty.Ok() @@ -39,12 +39,12 @@ var holotreePullCmd = &cobra.Command{ } func init() { - remoteOrigin := common.RccRemoteOrigin() + origin := common.RccRemoteOrigin() holotreeCmd.AddCommand(holotreePullCmd) holotreePullCmd.Flags().BoolVarP(&forcePull, "force", "", false, "Force pull check, even when blueprint is already present.") - holotreePullCmd.Flags().StringVarP(&remoteOrigin, "origin", "o", remoteOrigin, "URL of remote origin to pull environment from.") + holotreePullCmd.Flags().StringVarP(&remoteOriginOption, "origin", "o", origin, "URL of remote origin to pull environment from.") holotreePullCmd.Flags().StringVarP(&pullRobot, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file to export as catalog. ") - if len(remoteOrigin) == 0 { + if len(origin) == 0 { holotreePullCmd.MarkFlagRequired("origin") } } diff --git a/common/version.go b/common/version.go index 3f8b0d91..1bb652f6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.4.1` + Version = `v13.4.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3f590701..2d672730 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.4.2 (date: 31.1.2023) + +- fixed broken holotree pull command, and made it allow pulling from plain + http sources + ## v13.4.1 (date: 30.1.2023) - prebuild now needs shared holotree to be enabled before building diff --git a/operations/pull.go b/operations/pull.go index aeadd5cd..7c55404d 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -27,7 +27,7 @@ const ( func pullOriginFingerprints(origin, catalogName string) (fingerprints string, count int, err error) { defer fail.Around(&err) - client, err := cloud.NewClient(origin) + client, err := cloud.NewUnsafeClient(origin) fail.On(err != nil, "Could not create web client for %q, reason: %v", origin, err) request := client.NewRequest(fmt.Sprintf("/parts/%s", catalogName)) From 9ee5ea0cc59c89e13877dbfc3e8071316ebcaa11 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 1 Feb 2023 14:54:11 +0200 Subject: [PATCH 360/516] BUGFIX: cloud.ReadFile error fix (v13.4.3) - bugfix: shortcutting to file resource on cloud.ReadFile if actual exiting file is given as resource link. --- cloud/readfile.go | 5 ++++- cmd/holotreePrebuild.go | 27 ++++++++++++++++++++++++++- common/version.go | 2 +- conda/condayaml.go | 9 ++++++++- docs/changelog.md | 13 +++++++++---- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/cloud/readfile.go b/cloud/readfile.go index 3ae6f390..91556f9d 100644 --- a/cloud/readfile.go +++ b/cloud/readfile.go @@ -11,11 +11,14 @@ import ( ) func ReadFile(resource string) ([]byte, error) { + if pathlib.IsFile(resource) { + return os.ReadFile(resource) + } link, err := url.ParseRequestURI(resource) if err != nil { return os.ReadFile(resource) } - if link.Scheme == "file" || link.Scheme == "" { + if link.Scheme == "file" || link.Scheme == "" || pathlib.IsFile(link.Path) { return os.ReadFile(link.Path) } tempfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("temp%x.part", common.When)) diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index 427feed8..1e087e5a 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -2,6 +2,7 @@ package cmd import ( "net/url" + "os" "path/filepath" "strings" @@ -29,7 +30,7 @@ func conditionalExpand(filename string) string { return fullpath } -func resolveMetafile(link string) ([]string, error) { +func resolveMetafileURL(link string) ([]string, error) { origin, err := url.Parse(link) refok := err == nil raw, err := cloud.ReadFile(link) @@ -53,6 +54,30 @@ func resolveMetafile(link string) ([]string, error) { return result, nil } +func resolveMetafile(link string) ([]string, error) { + if !pathlib.IsFile(link) { + return resolveMetafileURL(link) + } + fullpath, err := filepath.Abs(link) + if err != nil { + return nil, err + } + basedir := filepath.Dir(fullpath) + raw, err := os.ReadFile(fullpath) + if err != nil { + return nil, err + } + result := []string{} + for _, line := range strings.SplitAfter(string(raw), "\n") { + flat := strings.TrimSpace(line) + if strings.HasPrefix(flat, "#") || len(flat) == 0 { + continue + } + result = append(result, filepath.Join(basedir, flat)) + } + return result, nil +} + func metafileExpansion(links []string, expand bool) []string { if !expand { return links diff --git a/common/version.go b/common/version.go index 1bb652f6..8f0b0221 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.4.2` + Version = `v13.4.3` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 5387a82c..67bc5107 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -551,7 +551,14 @@ func CondaYamlFrom(content []byte) (*Environment, error) { } func ReadCondaYaml(filename string) (*Environment, error) { - content, err := cloud.ReadFile(filename) + var content []byte + var err error + + if pathlib.IsFile(filename) { + content, err = os.ReadFile(filename) + } else { + content, err = cloud.ReadFile(filename) + } if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } diff --git a/docs/changelog.md b/docs/changelog.md index 2d672730..07079486 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,20 +1,25 @@ # rcc change log -## v13.4.2 (date: 31.1.2023) +## v13.4.3 (date: 1.2.2023) + +- bugfix: shortcutting to file resource on cloud.ReadFile if actual exiting + file is given as resource link. + +## v13.4.2 (date: 31.1.2023) UNSTABLE - fixed broken holotree pull command, and made it allow pulling from plain http sources -## v13.4.1 (date: 30.1.2023) +## v13.4.1 (date: 30.1.2023) UNSTABLE - prebuild now needs shared holotree to be enabled before building - prebuilds can now be forced for full rebuilds -## v13.4.0 (date: 30.1.2023) +## v13.4.0 (date: 30.1.2023) UNSTABLE - peercc is renamed to rccremote, and peercc package renamed to remotree -## v13.3.0 (date: 27.1.2023) +## v13.3.0 (date: 27.1.2023) UNSTABLE - feature: command for prebuilding environments (from files or from URLs) - improvement: rcc version visible in "Toplevel" command list From 29b97b138cf5defc3fae49c844450642567c1cfc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 2 Feb 2023 09:41:13 +0200 Subject: [PATCH 361/516] FEATURE: hololib fill for new environments (v13.5.0) - support for pulling hololib catalogs as part of normal holotree environment creation process (new Progress step). --- cmd/cloudPrepare.go | 2 +- cmd/holotreeBootstrap.go | 2 +- cmd/holotreePrebuild.go | 3 +- cmd/holotreePull.go | 2 +- cmd/holotreeVariables.go | 2 +- cmd/speed.go | 3 +- common/logger.go | 2 +- common/version.go | 2 +- conda/workflows.go | 18 +++++------ docs/changelog.md | 5 +++ htfs/commands.go | 35 +++++++++++++++------ operations/pull.go | 8 +++-- operations/running.go | 2 +- remotree/trigger.go | 2 +- robot_tests/export_holozip.robot | 4 +-- robot_tests/fullrun.robot | 20 ++++++------ robot_tests/unmanaged_space.robot | 52 +++++++++++++++---------------- 17 files changed, 96 insertions(+), 68 deletions(-) diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index dc992319..b44c9fd3 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -55,7 +55,7 @@ var prepareCloudCmd = &cobra.Command{ var label string condafile := config.CondaConfigFile() - label, _, err = htfs.NewEnvironment(condafile, config.Holozip(), true, false) + label, _, err = htfs.NewEnvironment(condafile, config.Holozip(), true, false, operations.PullCatalog) pretty.Guard(err == nil, 8, "Error: %v", err) common.Log("Prepared %q.", label) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 90deeba2..af5c174a 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -37,7 +37,7 @@ func updateEnvironments(robots []string) { if !config.UsesConda() { continue } - _, _, err = htfs.NewEnvironment(config.CondaConfigFile(), "", false, false) + _, _, err = htfs.NewEnvironment(config.CondaConfigFile(), "", false, false, operations.PullCatalog) pretty.Guard(err == nil, 2, "Holotree recording error: %v", err) } } diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index 1e087e5a..1891aeda 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -110,7 +111,7 @@ var holotreePrebuildCmd = &cobra.Command{ total, failed := len(configurations), 0 for at, configfile := range configurations { pretty.Note("%d/%d: Now building config %q", at+1, total, configfile) - _, _, err := htfs.NewEnvironment(configfile, "", false, forceBuild) + _, _, err := htfs.NewEnvironment(configfile, "", false, forceBuild, operations.PullCatalog) if err != nil { failed += 1 pretty.Warning("%d/%d: Holotree recording error: %v", at+1, total, err) diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index 8a00a21a..2336ab92 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -31,7 +31,7 @@ var holotreePullCmd = &cobra.Command{ present := tree.HasBlueprint(holotreeBlueprint) if !present || forcePull { catalog := htfs.CatalogName(hash) - err = operations.PullCatalog(remoteOriginOption, catalog) + err = operations.PullCatalog(remoteOriginOption, catalog, true) pretty.Guard(err == nil, 3, "%s", err) } pretty.Ok() diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 2eb6d6a6..cd675e2b 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -79,7 +79,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp if config != nil { holozip = config.Holozip() } - path, _, err := htfs.NewEnvironment(condafile, holozip, true, force) + path, _, err := htfs.NewEnvironment(condafile, holozip, true, force, operations.PullCatalog) pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/cmd/speed.go b/cmd/speed.go index 6e39339a..81e16fee 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -11,6 +11,7 @@ import ( "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -72,7 +73,7 @@ var speedtestCmd = &cobra.Command{ pretty.Exit(2, "Error: %v", err) } common.ForcedRobocorpHome = folder - _, score, err := htfs.NewEnvironment(condafile, "", true, true) + _, score, err := htfs.NewEnvironment(condafile, "", true, true, operations.PullCatalog) common.Silent, common.TraceFlag, common.DebugFlag = silent, trace, debug common.UnifyVerbosityFlags() if err != nil { diff --git a/common/logger.go b/common/logger.go index cffce196..7fe2d74a 100644 --- a/common/logger.go +++ b/common/logger.go @@ -104,6 +104,6 @@ func Progress(step int, form string, details ...interface{}) { ProgressMark = time.Now() delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() message := fmt.Sprintf(form, details...) - Log("#### Progress: %02d/13 %s %8.3fs %s", step, Version, delta, message) + Log("#### Progress: %02d/14 %s %8.3fs %s", step, Version, delta, message) Timeline("%d/13 %s", step, message) } diff --git a/common/version.go b/common/version.go index 8f0b0221..2f1e8b7e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.4.3` + Version = `v13.5.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 9a9bcd7b..4f611348 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -150,7 +150,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - common.Progress(5, "Running micromamba phase. (micromamba v%s)", MicromambaVersion()) + common.Progress(6, "Running micromamba phase. (micromamba v%s)", MicromambaVersion()) mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -180,7 +180,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - common.Progress(6, "Skipping pip install phase -- no pip dependencies.") + common.Progress(7, "Skipping pip install phase -- no pip dependencies.") } else { if !pyok { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) @@ -188,7 +188,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) return false, false } - common.Progress(6, "Running pip install phase. (pip v%s)", PipVersion(python)) + common.Progress(7, "Running pip install phase. (pip v%s)", PipVersion(python)) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander(python, "-m", "pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -209,7 +209,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - common.Progress(7, "Post install scripts phase started.") + common.Progress(8, "Post install scripts phase started.") common.Debug("=== post install phase ===") for _, script := range postInstall { scriptCommand, err := shell.Split(script) @@ -229,9 +229,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } journal.CurrentBuildEvent().PostInstallComplete() } else { - common.Progress(7, "Post install scripts phase skipped -- no scripts.") + common.Progress(8, "Post install scripts phase skipped -- no scripts.") } - common.Progress(8, "Activate environment started phase.") + common.Progress(9, "Activate environment started phase.") common.Debug("=== activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) err = Activate(planWriter, targetFolder) @@ -247,7 +247,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- pip check plan @%ss ---\n\n", stopwatch) if common.StrictFlag && pipUsed { - common.Progress(9, "Running pip check phase.") + common.Progress(10, "Running pip check phase.") pipCommand := common.NewCommander(python, "-m", "pip", "check", "--no-color") pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") @@ -261,12 +261,12 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } common.Timeline("pip check done.") } else { - common.Progress(9, "Pip check skipped.") + common.Progress(10, "Pip check skipped.") } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planSink.Sync() planSink.Close() - common.Progress(10, "Update installation plan.") + common.Progress(11, "Update installation plan.") finalplan := filepath.Join(targetFolder, "rcc_plan.log") os.Rename(planfile, finalplan) common.Debug("=== finalize phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 07079486..20944f04 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.5.0 (date: 2.2.2023) + +- support for pulling hololib catalogs as part of normal holotree environment + creation process (new Progress step). + ## v13.4.3 (date: 1.2.2023) - bugfix: shortcutting to file resource on cloud.ReadFile if actual exiting diff --git a/htfs/commands.go b/htfs/commands.go index cc9054cb..8fecf66e 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -17,7 +17,9 @@ import ( "github.com/robocorp/rcc/xviper" ) -func NewEnvironment(condafile, holozip string, restore, force bool) (label string, scorecard common.Scorecard, err error) { +type CatalogPuller func(string, string, bool) error + +func NewEnvironment(condafile, holozip string, restore, force bool, puller CatalogPuller) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) journal.CurrentBuildEvent().StartNow(force) @@ -33,7 +35,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin path := "" defer func() { - common.Progress(13, "Fresh holotree done [with %d workers].", anywork.Scale()) + common.Progress(14, "Fresh holotree done [with %d workers].", anywork.Scale()) if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) } @@ -85,18 +87,18 @@ func NewEnvironment(condafile, holozip string, restore, force bool) (label strin common.Timeline("downgraded to holotree zip library") } else { scorecard.Start() - err = RecordEnvironment(tree, holotreeBlueprint, force, scorecard) + err = RecordEnvironment(tree, holotreeBlueprint, force, scorecard, puller) fail.On(err != nil, "%s", err) library = tree } if restore { - common.Progress(12, "Restore space from library [with %d workers].", anywork.Scale()) + common.Progress(13, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() } else { - common.Progress(12, "Restoring space skipped.") + common.Progress(13, "Restoring space skipped.") } return path, scorecard, nil @@ -108,7 +110,7 @@ func CleanupHolotreeStage(tree MutableLibrary) error { return pathlib.TryRemoveAll("stage", tree.Stage()) } -func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorecard common.Scorecard) (err error) { +func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorecard common.Scorecard, puller CatalogPuller) (err error) { defer fail.Around(&err) // following must be setup here @@ -124,7 +126,22 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec common.Debug("Has blueprint environment: %v", exists) if force || !exists { - common.Progress(3, "Cleanup holotree stage for fresh install.") + remoteOrigin := common.RccRemoteOrigin() + if len(remoteOrigin) > 0 { + common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN.") + hash := BlueprintHash(blueprint) + catalog := CatalogName(hash) + err = puller(remoteOrigin, catalog, false) + if err != nil { + pretty.Warning("Failed to pull %q from %q, reason: %v", catalog, remoteOrigin, err) + } else { + return nil + } + exists = tree.HasBlueprint(blueprint) + } else { + common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN skipped. RCC_REMOTE_ORIGIN was not defined.") + } + common.Progress(4, "Cleanup holotree stage for fresh install.") fail.On(settings.Global.NoBuild(), "Building new holotree environment is blocked by settings, and could not be found from hololib cache!") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) @@ -133,7 +150,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) - common.Progress(4, "Build environment into holotree stage.") + common.Progress(5, "Build environment into holotree stage.") identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = os.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) @@ -142,7 +159,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec scorecard.Midpoint() - common.Progress(11, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) + common.Progress(12, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) journal.CurrentBuildEvent().RecordComplete() diff --git a/operations/pull.go b/operations/pull.go index 7c55404d..8db7b130 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -112,7 +112,7 @@ func ProtectedImport(filename string) (err error) { return Unzip(common.HololibLocation(), filename, true, false, false) } -func PullCatalog(origin, catalogName string) (err error) { +func PullCatalog(origin, catalogName string, useLock bool) (err error) { defer fail.Around(&err) common.Timeline("pull %q parts from %q", catalogName, origin) @@ -126,7 +126,11 @@ func PullCatalog(origin, catalogName string) (err error) { common.Debug("Temporary content based filename is: %q", filename) defer pathlib.TryRemove("temporary", filename) - err = ProtectedImport(filename) + if useLock { + err = ProtectedImport(filename) + } else { + err = Unzip(common.HololibLocation(), filename, true, false, false) + } fail.On(err != nil, "Failed to unzip %v to hololib, reason: %v", filename, err) common.Timeline("environment pull completed") diff --git a/operations/running.go b/operations/running.go index b8135016..abf1b93d 100644 --- a/operations/running.go +++ b/operations/running.go @@ -172,7 +172,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. return true, config, todo, "" } - label, _, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force) + label, _, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force, PullCatalog) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/remotree/trigger.go b/remotree/trigger.go index 2d2c3bf6..3884fb15 100644 --- a/remotree/trigger.go +++ b/remotree/trigger.go @@ -20,7 +20,7 @@ func makeTriggerHandler(requests chan string) http.HandlerFunc { func pullOperation(counter int, catalog, remoteOrigin string) { defer common.Stopwatch("#%d: pull opearation lasted", counter).Report() common.Log("#%d: Trying to pull %q from %q ...", counter, catalog, remoteOrigin) - err := operations.PullCatalog(remoteOrigin, catalog) + err := operations.PullCatalog(remoteOrigin, catalog, true) if err != nil { pretty.Warning("#%d: Failed to pull %q from %q, reason: %v", counter, catalog, remoteOrigin, err) } else { diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 8eb2bff7..8ea6c1a8 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -35,8 +35,8 @@ Goal: Create environment for standalone robot Must Have RCC_INSTALLATION_ID= Must Have 4e67cd8_fcb4b859 Use STDERR - Must Have Progress: 01/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 14/14 Goal: Must have author space visible Step build/rcc ht ls diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index a06395fc..aaffa377 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -82,9 +82,9 @@ Goal: Run task in place in debug mode and with timeline. Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR - Must Have Progress: 01/13 - Must Have Progress: 02/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 02/14 + Must Have Progress: 14/14 Must Have rpaframework Must Have PID # Must Have [N] @@ -115,13 +115,13 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 01/13 - Wont Have Progress: 03/13 - Wont Have Progress: 05/13 - Wont Have Progress: 07/13 - Wont Have Progress: 09/13 - Must Have Progress: 12/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Wont Have Progress: 03/14 + Wont Have Progress: 05/14 + Wont Have Progress: 07/14 + Wont Have Progress: 09/14 + Must Have Progress: 13/14 + Must Have Progress: 14/14 Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/unmanaged_space.robot b/robot_tests/unmanaged_space.robot index 611fa7ac..244356fe 100644 --- a/robot_tests/unmanaged_space.robot +++ b/robot_tests/unmanaged_space.robot @@ -33,13 +33,13 @@ Goal: See variables from specific unamanged space Wont Have ROBOT_ARTIFACTS= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/13 - Must Have Progress: 02/13 - Must Have Progress: 04/13 - Must Have Progress: 05/13 - Must Have Progress: 06/13 - Must Have Progress: 12/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 02/14 + Must Have Progress: 04/14 + Must Have Progress: 05/14 + Must Have Progress: 06/14 + Must Have Progress: 13/14 + Must Have Progress: 14/14 Goal: Wont allow use of unmanaged space with incompatible conda.yaml Step build/rcc holotree variables --debug --unmanaged --space python39 --controller citests robot_tests/python375.yaml 6 @@ -49,14 +49,14 @@ Goal: Wont allow use of unmanaged space with incompatible conda.yaml Wont Have RCC_INSTALLATION_ID= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/13 - Must Have Progress: 02/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 02/14 + Must Have Progress: 14/14 - Wont Have Progress: 04/13 - Wont Have Progress: 05/13 - Wont Have Progress: 06/13 - Wont Have Progress: 12/13 + Wont Have Progress: 04/14 + Wont Have Progress: 05/14 + Wont Have Progress: 06/14 + Wont Have Progress: 13/14 Must Have Existing unmanaged space fingerprint Must Have does not match requested one @@ -84,24 +84,24 @@ Goal: Allows different unmanaged space for different conda.yaml Wont Have ROBOT_ARTIFACTS= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/13 - Must Have Progress: 02/13 - Must Have Progress: 04/13 - Must Have Progress: 05/13 - Must Have Progress: 06/13 - Must Have Progress: 12/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 02/14 + Must Have Progress: 04/14 + Must Have Progress: 05/14 + Must Have Progress: 06/14 + Must Have Progress: 13/14 + Must Have Progress: 14/14 Goal: Wont allow use of unmanaged space with incompatible conda.yaml when two unmanaged spaces exists Step build/rcc holotree variables --debug --unmanaged --space python37 --controller citests robot_tests/python3913.yaml 6 Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/13 - Must Have Progress: 02/13 - Must Have Progress: 13/13 + Must Have Progress: 01/14 + Must Have Progress: 02/14 + Must Have Progress: 14/14 - Wont Have Progress: 05/13 - Wont Have Progress: 12/13 + Wont Have Progress: 05/14 + Wont Have Progress: 13/14 Must Have Existing unmanaged space fingerprint Must Have does not match requested one From 0a4d0eba8aed862abeffb45960d3a43cfb58a75d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 2 Feb 2023 11:23:43 +0200 Subject: [PATCH 362/516] FIX: hololib fill improvements (v13.5.1) - fixing progress counter on timeline output - timeline output clarifications on hololib pull step --- common/logger.go | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/pull.go | 7 +++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/common/logger.go b/common/logger.go index 7fe2d74a..26143a22 100644 --- a/common/logger.go +++ b/common/logger.go @@ -105,5 +105,5 @@ func Progress(step int, form string, details ...interface{}) { delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() message := fmt.Sprintf(form, details...) Log("#### Progress: %02d/14 %s %8.3fs %s", step, Version, delta, message) - Timeline("%d/13 %s", step, message) + Timeline("%d/14 %s", step, message) } diff --git a/common/version.go b/common/version.go index 2f1e8b7e..04a9c275 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.0` + Version = `v13.5.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 20944f04..b070bcbc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.5.1 (date: 2.2.2023) + +- fixing progress counter on timeline output +- timeline output clarifications on hololib pull step + ## v13.5.0 (date: 2.2.2023) - support for pulling hololib catalogs as part of normal holotree environment diff --git a/operations/pull.go b/operations/pull.go index 8db7b130..48d253f4 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -115,7 +115,11 @@ func ProtectedImport(filename string) (err error) { func PullCatalog(origin, catalogName string, useLock bool) (err error) { defer fail.Around(&err) - common.Timeline("pull %q parts from %q", catalogName, origin) + common.TimelineBegin("hololib+catalog pull start") + defer common.TimelineEnd() + + common.Timeline("pulling %q parts from %q", catalogName, origin) + unknownSelected, count, err := pullOriginFingerprints(origin, catalogName) fail.On(err != nil, "%v", err) @@ -132,7 +136,6 @@ func PullCatalog(origin, catalogName string, useLock bool) (err error) { err = Unzip(common.HololibLocation(), filename, true, false, false) } fail.On(err != nil, "Failed to unzip %v to hololib, reason: %v", filename, err) - common.Timeline("environment pull completed") return nil } From 7a32520e06e10ef43aa21cd20cd0946cacc2abd4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 2 Feb 2023 17:16:05 +0200 Subject: [PATCH 363/516] FIX: rccremote server timeouts (v13.5.2) - rccremote server timeout adjustments to much longer times (experimental) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ remotree/server.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 04a9c275..b001e836 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.1` + Version = `v13.5.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index b070bcbc..1323e812 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.5.2 (date: 2.2.2023) + +- rccremote server timeout adjustments to much longer times (experimental) + ## v13.5.1 (date: 2.2.2023) - fixing progress counter on timeline output diff --git a/remotree/server.go b/remotree/server.go index 06102e1b..58c1c6fa 100644 --- a/remotree/server.go +++ b/remotree/server.go @@ -37,8 +37,8 @@ func Serve(address string, port int, domain, storage string) error { server := &http.Server{ Addr: listen, Handler: mux, - ReadTimeout: 20 * time.Second, - WriteTimeout: 40 * time.Second, + ReadTimeout: 2 * time.Minute, + WriteTimeout: 30 * time.Minute, MaxHeaderBytes: 1 << 14, } From b8fd605651f007b5cb069e88318065e3afd0b041 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 6 Feb 2023 12:17:32 +0200 Subject: [PATCH 364/516] IMPROVEMENT: rccremote zipfile cache (v13.5.3) - rccremote server zip file managementent improvements --- common/algorithms.go | 6 +++ common/version.go | 2 +- docs/changelog.md | 4 ++ operations/zipper.go | 2 +- pathlib/functions.go | 24 ++++++++++++ remotree/delta.go | 87 ++++++++++++++++++++++++++++++++------------ remotree/server.go | 8 ++++ 7 files changed, 108 insertions(+), 25 deletions(-) diff --git a/common/algorithms.go b/common/algorithms.go index bb03e94f..4904c97c 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -41,6 +41,12 @@ func ShortDigest(content string) string { return result[:16] } +func Digest(content string) string { + digester := sha256.New() + digester.Write([]byte(content)) + return Hexdigest(digester.Sum(nil)) +} + func Siphash(left, right uint64, body []byte) uint64 { return siphash.Hash(left, right, body) } diff --git a/common/version.go b/common/version.go index b001e836..76ae6b6a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.2` + Version = `v13.5.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1323e812..1aea673d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.5.3 (date: 6.2.2023) UNSTABLE + +- rccremote server zip file managementent improvements + ## v13.5.2 (date: 2.2.2023) - rccremote server timeout adjustments to much longer times (experimental) diff --git a/operations/zipper.go b/operations/zipper.go index 6e2faa80..d873f0be 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -353,7 +353,7 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { } func Unzip(directory, zipfile string, force, temporary, flatten bool) error { - common.Timeline("unzip %q to %q", zipfile, directory) + common.Timeline("unzip %q [size: %s] to %q", zipfile, pathlib.HumaneSize(zipfile), directory) defer common.Timeline("unzip done") fullpath, err := filepath.Abs(directory) if err != nil { diff --git a/pathlib/functions.go b/pathlib/functions.go index ca9d3892..f696c2f3 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -88,6 +88,30 @@ func Size(pathname string) (int64, bool) { return stat.Size(), true } +func kiloShift(size float64) float64 { + return size / 1024.0 +} + +func HumaneSize(pathname string) string { + rawsize, ok := Size(pathname) + if !ok { + return "N/A" + } + kilos := kiloShift(float64(rawsize)) + if kilos < 1.0 { + return fmt.Sprintf("%db", rawsize) + } + megas := kiloShift(kilos) + if megas < 1.0 { + return fmt.Sprintf("%3.1fK", kilos) + } + gigas := kiloShift(megas) + if gigas < 1.0 { + return fmt.Sprintf("%3.1fM", megas) + } + return fmt.Sprintf("%3.1fG", gigas) +} + func Modtime(pathname string) (time.Time, error) { stat, err := os.Stat(pathname) if err != nil { diff --git a/remotree/delta.go b/remotree/delta.go index a7263356..b955b4de 100644 --- a/remotree/delta.go +++ b/remotree/delta.go @@ -3,14 +3,18 @@ package remotree import ( "archive/zip" "bufio" + "fmt" "io" "net/http" + "os" "path/filepath" "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/set" ) @@ -74,33 +78,70 @@ func makeDeltaHandler(queries Partqueries) http.HandlerFunc { } } - headers := response.Header() - headers.Add("Content-Type", "application/zip") - response.WriteHeader(http.StatusOK) - - sink := zip.NewWriter(response) - defer sink.Close() - - for _, member := range approved { - relative := htfs.RelativeDefaultLocation(member) - fullpath := htfs.ExactDefaultLocation(member) - err := operations.ZipAppend(sink, fullpath, relative) - if err != nil { - common.Debug("DELTA: error %v with %v -> %v", err, fullpath, relative) - return - } - } - - fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) - relative, err := filepath.Rel(common.HololibLocation(), fullpath) + partfile, err := exportMissing(catalog, approved) if err != nil { common.Debug("DELTA: error %v", err) + response.WriteHeader(http.StatusInternalServerError) return } + + http.ServeFile(response, request, partfile) + } +} + +func tempDir() (string, bool) { + root := pathlib.TempDir() + directory := filepath.Join(root, "rccremote") + fullpath, err := pathlib.EnsureDirectory(directory) + if err != nil { + return root, false + } + return fullpath, true +} + +func exportMissing(catalog string, missing []string) (result string, err error) { + defer fail.Around(&err) + + tempdir, _ := tempDir() + identity := common.Digest(strings.Join(missing, "\n")) + filename := filepath.Join(tempdir, fmt.Sprintf("%s_parts.zip", identity)) + if pathlib.IsFile(filename) { + common.Debug("Using existing cache file %q [size: %s]", filename, pathlib.HumaneSize(filename)) + return filename, nil + } + + tempfile := filepath.Join(tempdir, fmt.Sprintf("%s_%x_build.zip", identity, os.Getppid())) + err = exportMissingToFile(catalog, missing, tempfile) + fail.On(err != nil, "%v", err) + + err = os.Rename(tempfile, filename) + fail.On(err != nil, "%v", err) + + common.Debug("Created cache file %q [size: %s]", filename, pathlib.HumaneSize(filename)) + return filename, nil +} + +func exportMissingToFile(catalog string, missing []string, filename string) (err error) { + defer fail.Around(&err) + + handle, err := os.Create(filename) + fail.On(err != nil, "Could not create export file %q, reason: %v", filename, err) + defer handle.Close() + + sink := zip.NewWriter(handle) + defer sink.Close() + + for _, member := range missing { + relative := htfs.RelativeDefaultLocation(member) + fullpath := htfs.ExactDefaultLocation(member) err = operations.ZipAppend(sink, fullpath, relative) - if err != nil { - common.Debug("DELTA: error %v", err) - return - } + fail.On(err != nil, "Could not zip file %q, reason: %v", fullpath, err) } + + fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) + relative, err := filepath.Rel(common.HololibLocation(), fullpath) + fail.On(err != nil, "Could not get relative path for catalog %q, reason: %v", fullpath, err) + err = operations.ZipAppend(sink, fullpath, relative) + fail.On(err != nil, "Could not zip catalog %q, reason: %v", fullpath, err) + return nil } diff --git a/remotree/server.go b/remotree/server.go index 58c1c6fa..e72ba599 100644 --- a/remotree/server.go +++ b/remotree/server.go @@ -9,6 +9,8 @@ import ( "path/filepath" "syscall" "time" + + "github.com/robocorp/rcc/pathlib" ) func Serve(address string, port int, domain, storage string) error { @@ -23,6 +25,12 @@ func Serve(address string, port int, domain, storage string) error { } defer cleanupHoldStorage(holding) + tempdir, ok := tempDir() + if ok { + pathlib.TryRemoveAll("remotree.Serve[start]", tempdir) + defer pathlib.TryRemoveAll("remotree.Serve[defer]", tempdir) + } + triggers := make(chan string, 20) defer close(triggers) From e61af3614619e9e71f42ced9d783f549d3407313 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 6 Feb 2023 16:37:31 +0200 Subject: [PATCH 365/516] BUGFIX: rcc holotree pull fix (v13.5.4) - bugfix: file syncing on pull commands --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/pull.go | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 76ae6b6a..a347bcab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.3` + Version = `v13.5.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1aea673d..a50668a9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.5.4 (date: 6.2.2023) UNSTABLE + +- bugfix: file syncing on pull commands + ## v13.5.3 (date: 6.2.2023) UNSTABLE - rccremote server zip file managementent improvements diff --git a/operations/pull.go b/operations/pull.go index 48d253f4..eaad1aba 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -79,7 +79,6 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil out, err := os.Create(filename) fail.On(err != nil, "Creating temporary file %q failed, reason: %v", filename, err) - defer out.Close() defer pathlib.TryRemove("temporary", filename) digest := sha256.New() @@ -90,6 +89,12 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil _, err = io.Copy(many, response.Body) fail.On(err != nil, "Download failed, reason: %v", err) + err = out.Sync() + fail.On(err != nil, "Sync of %q failed, reason: %v", filename, err) + + err = out.Close() + fail.On(err != nil, "Closing %q failed, reason: %v", filename, err) + sum := fmt.Sprintf("%02x", digest.Sum(nil)) finalname := filepath.Join(pathlib.TempDir(), fmt.Sprintf("rccremote_%s.zip", sum)) err = pathlib.TryRename("delta", filename, finalname) From 56c4bd10c213552c3a4c3a6606e7119f87b8ba08 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Feb 2023 11:19:13 +0200 Subject: [PATCH 366/516] BUGFIX: Windows long path check fix (v13.5.5) - bugfix: contain output of checking long path support on Windows - improvement: adding more structure to holotree pull timeline --- common/timeline.go | 2 +- common/version.go | 2 +- conda/platform_windows.go | 2 +- docs/changelog.md | 5 +++++ operations/pull.go | 17 ++++++++++++++--- operations/zipper.go | 5 +++-- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/common/timeline.go b/common/timeline.go index a4810a57..90b31429 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -82,7 +82,7 @@ func TimelineBegin(form string, details ...interface{}) { func TimelineEnd() { indent <- false - Timeline("`") + Timeline("`--") } func EndOfTimeline() { diff --git a/common/version.go b/common/version.go index a347bcab..daefc83f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.4` + Version = `v13.5.5` ) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 72943b5c..86090914 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -74,7 +74,7 @@ func HasLongPathSupport() bool { } fullpath := filepath.Join(baseline...) - code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).Transparent() + code, err := shell.New(nil, ".", "cmd.exe", "/c", "mkdir", fullpath).StderrOnly().Transparent() common.Trace("Checking long path support with MKDIR '%v' (%d characters) -> %v [%v] {%d}", fullpath, len(fullpath), err == nil, err, code) if err != nil { longPathSupportArticle := settings.Global.DocsLink("product-manuals/robocorp-lab/troubleshooting#windows-has-to-have-long-filenames-support-on") diff --git a/docs/changelog.md b/docs/changelog.md index a50668a9..38c01f62 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.5.5 (date: 7.2.2023) + +- bugfix: contain output of checking long path support on Windows +- improvement: adding more structure to holotree pull timeline + ## v13.5.4 (date: 6.2.2023) UNSTABLE - bugfix: file syncing on pull commands diff --git a/operations/pull.go b/operations/pull.go index eaad1aba..4ae63fc1 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -27,12 +27,18 @@ const ( func pullOriginFingerprints(origin, catalogName string) (fingerprints string, count int, err error) { defer fail.Around(&err) + common.TimelineBegin("pull rccremote origin fingerprints") + defer common.TimelineEnd() + client, err := cloud.NewUnsafeClient(origin) fail.On(err != nil, "Could not create web client for %q, reason: %v", origin, err) + url := fmt.Sprintf("%s/parts/%s", origin, catalogName) request := client.NewRequest(fmt.Sprintf("/parts/%s", catalogName)) request.Headers[X_RCC_RANDOM_IDENTITY] = common.RandomIdentifier() response := client.Get(request) + common.Timeline("status %d from GET %q", response.Status, url) + fail.On(response.Status != 200, "Problem with parts request, status=%d, body=%s", response.Status, response.Body) stream := bufio.NewReader(bytes.NewReader(response.Body)) @@ -47,6 +53,7 @@ func pullOriginFingerprints(origin, catalogName string) (fingerprints string, co } } if err == io.EOF { + common.Timeline("total of %d parts in catalog %q", len(collection), catalogName) return strings.Join(collection, "\n"), len(collection), nil } fail.On(err != nil, "STREAM error: %v", err) @@ -55,9 +62,12 @@ func pullOriginFingerprints(origin, catalogName string) (fingerprints string, co return "", 0, fmt.Errorf("Unexpected reach of code that should never happen.") } -func downloadMissingEnvironmentParts(origin, catalogName, selection string) (filename string, err error) { +func downloadMissingEnvironmentParts(count int, origin, catalogName, selection string) (filename string, err error) { defer fail.Around(&err) + common.TimelineBegin("download %d parts + catalog from %q", count, origin) + defer common.TimelineEnd() + url := fmt.Sprintf("%s/delta/%s", origin, catalogName) body := strings.NewReader(selection) @@ -75,6 +85,8 @@ func downloadMissingEnvironmentParts(origin, catalogName, selection string) (fil fail.On(err != nil, "Web request to %q failed, reason: %v", url, err) defer response.Body.Close() + common.Timeline("status %d from POST %q", response.StatusCode, url) + fail.On(response.StatusCode < 200 || 299 < response.StatusCode, "%s (%s)", response.Status, url) out, err := os.Create(filename) @@ -128,8 +140,7 @@ func PullCatalog(origin, catalogName string, useLock bool) (err error) { unknownSelected, count, err := pullOriginFingerprints(origin, catalogName) fail.On(err != nil, "%v", err) - common.Timeline("download %d parts + catalog from %q", count, origin) - filename, err := downloadMissingEnvironmentParts(origin, catalogName, unknownSelected) + filename, err := downloadMissingEnvironmentParts(count, origin, catalogName, unknownSelected) fail.On(err != nil, "%v", err) common.Debug("Temporary content based filename is: %q", filename) diff --git a/operations/zipper.go b/operations/zipper.go index d873f0be..b7a1e88c 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -353,8 +353,9 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { } func Unzip(directory, zipfile string, force, temporary, flatten bool) error { - common.Timeline("unzip %q [size: %s] to %q", zipfile, pathlib.HumaneSize(zipfile), directory) - defer common.Timeline("unzip done") + common.TimelineBegin("unzip %q [size: %s] to %q", zipfile, pathlib.HumaneSize(zipfile), directory) + defer common.TimelineEnd() + fullpath, err := filepath.Abs(directory) if err != nil { return err From 971031526de58cf800f2fd343aa8408e44e26a6a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Feb 2023 13:13:38 +0200 Subject: [PATCH 367/516] BUGFIX: ensure directories exists (v13.5.6) - bugfix: create missing folders while creating and writing some files - improvement: added optional top N biggest files sizes on catalog listing --- cloud/client.go | 4 +--- cmd/holotreeCatalogs.go | 41 ++++++++++++++++++++++++++++++++++---- cmd/holotreeVariables.go | 4 ++-- cmd/rcc/main.go | 2 +- cmd/root.go | 2 +- cmd/speed.go | 3 ++- common/version.go | 2 +- conda/activate.go | 5 +++-- conda/condayaml.go | 4 ++-- conda/dependencies.go | 3 ++- conda/workflows.go | 4 ++-- docs/changelog.md | 5 +++++ htfs/directory.go | 30 +++++++++++++++++++++++++++- htfs/library.go | 2 +- operations/cache.go | 2 +- operations/community.go | 4 ++-- operations/fixing.go | 2 +- operations/pull.go | 2 +- operations/updownload.go | 2 +- operations/zipper.go | 8 ++------ pathlib/functions.go | 43 +++++++++++++++++++++++++++++++--------- pathlib/touch.go | 2 +- remotree/delta.go | 2 +- settings/profile.go | 4 ++-- shell/task.go | 5 +++-- 25 files changed, 138 insertions(+), 49 deletions(-) diff --git a/cloud/client.go b/cloud/client.go index c0ad44b2..d92bb6e0 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "os" - "path/filepath" "strings" "time" @@ -226,8 +225,7 @@ func Download(url, filename string) error { return fmt.Errorf("Downloading %q failed, reason: %q!", url, response.Status) } - pathlib.EnsureDirectory(filepath.Dir(filename)) - out, err := os.Create(filename) + out, err := pathlib.Create(filename) if err != nil { return err } diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index c17e3fca..302d0c4f 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "text/tabwriter" @@ -12,11 +13,13 @@ import ( "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" "github.com/spf13/cobra" ) var ( showIdentityYaml bool + topSizes int ) const mega = 1024 * 1024 @@ -67,7 +70,7 @@ func identityContentLines(catalog *htfs.Root) []string { return result } -func jsonCatalogDetails(roots []*htfs.Root) { +func jsonCatalogDetails(roots []*htfs.Root, topN int) { used := catalogUsedStats() holder := make(map[string]map[string]interface{}) for _, catalog := range roots { @@ -86,6 +89,9 @@ func jsonCatalogDetails(roots []*htfs.Root) { if showIdentityYaml { data["identity-content"] = identityContent(catalog) } + if topN > 0 { + data[fmt.Sprintf("top%d", topN)] = catalog.Top(topN) + } data["platform"] = catalog.Platform data["directories"] = stats.Directories data["files"] = stats.Files @@ -100,7 +106,30 @@ func jsonCatalogDetails(roots []*htfs.Root) { common.Stdout("%s\n", nice) } -func listCatalogDetails(roots []*htfs.Root) { +func percent(value, base float64) float64 { + if base == 0.0 { + return 0.0 + } + return 100.0 * value / base +} + +func dumpTopN(stats map[string]int64, total float64, tabbed *tabwriter.Writer) { + sizes := set.Values(stats) + sort.Slice(sizes, func(left, right int) bool { + return sizes[left] > sizes[right] + }) + for _, focus := range sizes { + share := percent(float64(focus), total) + value, suffix := pathlib.HumaneSizer(focus) + for filename, size := range stats { + if focus == size { + tabbed.Write([]byte(fmt.Sprintf("\t\t\t%5.1f%%\t%6.1f%s\t%s\n", share, value, suffix, filename))) + } + } + } +} + +func listCatalogDetails(roots []*htfs.Root, topN int) { used := catalogUsedStats() tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside hololib)\tHolotree path\tAge (days)\tIdle (days)\n")) @@ -121,6 +150,9 @@ func listCatalogDetails(roots []*htfs.Root) { tabbed.Write([]byte(fmt.Sprintf("\t\t\t\t\t%s\n", line))) } } + if topN > 0 { + dumpTopN(catalog.Top(topN), float64(stats.Bytes), tabbed) + } } tabbed.Flush() } @@ -135,9 +167,9 @@ var holotreeCatalogsCmd = &cobra.Command{ } _, roots := htfs.LoadCatalogs() if jsonFlag { - jsonCatalogDetails(roots) + jsonCatalogDetails(roots, topSizes) } else { - listCatalogDetails(roots) + listCatalogDetails(roots, topSizes) } pretty.Ok() }, @@ -147,4 +179,5 @@ func init() { holotreeCmd.AddCommand(holotreeCatalogsCmd) holotreeCatalogsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") holotreeCatalogsCmd.Flags().BoolVarP(&showIdentityYaml, "identity", "i", false, "Show identity.yaml in catalog context.") + holotreeCatalogsCmd.Flags().IntVarP(&topSizes, "top", "t", 0, "Show top N sized files from catalog") } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index cd675e2b..e264f2d8 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "path/filepath" "strings" @@ -11,6 +10,7 @@ import ( "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/robot" "github.com/spf13/cobra" @@ -72,7 +72,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp pretty.Guard(err == nil, 5, "%s", err) condafile := filepath.Join(common.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) - err = os.WriteFile(condafile, holotreeBlueprint, 0o644) + err = pathlib.WriteFile(condafile, holotreeBlueprint, 0o644) pretty.Guard(err == nil, 6, "%s", err) holozip := "" diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index b3fc3346..612826bf 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -86,7 +86,7 @@ func markTempForRecycling() { target := common.RobocorpTempName() if pathlib.Exists(target) { filename := filepath.Join(target, "recycle.now") - os.WriteFile(filename, []byte("True"), 0o644) + pathlib.WriteFile(filename, []byte("True"), 0o644) common.Debug("Marked %q for recycling.", target) markedAlready = true } diff --git a/cmd/root.go b/cmd/root.go index 97885d37..d26c86ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -124,7 +124,7 @@ func init() { func initConfig() { if profilefile != "" { common.TimelineBegin("profiling run started") - sink, err := os.Create(profilefile) + sink, err := pathlib.Create(profilefile) pretty.Guard(err == nil, 5, "Failed to create profile file %q, reason %v.", profilefile, err) err = pprof.StartCPUProfile(sink) pretty.Guard(err == nil, 6, "Failed to start CPU profile, reason %v.", err) diff --git a/cmd/speed.go b/cmd/speed.go index 81e16fee..7a14c5f1 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -68,7 +69,7 @@ var speedtestCmd = &cobra.Command{ pretty.Exit(1, "Error: %v", err) } condafile := filepath.Join(folder, "speedtest.yaml") - err = os.WriteFile(condafile, content, 0o666) + err = pathlib.WriteFile(condafile, content, 0o666) if err != nil { pretty.Exit(2, "Error: %v", err) } diff --git a/common/version.go b/common/version.go index daefc83f..a023f4d5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.5` + Version = `v13.5.6` ) diff --git a/conda/activate.go b/conda/activate.go index 217aef28..6c0971cc 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" ) const ( @@ -62,7 +63,7 @@ func createScript(targetFolder string) (string, error) { script.Execute(buffer, details) scriptfile := filepath.Join(targetFolder, fmt.Sprintf("rcc_activate%s", commandSuffix)) - err = os.WriteFile(scriptfile, buffer.Bytes(), 0o755) + err = pathlib.WriteFile(scriptfile, buffer.Bytes(), 0o755) if err != nil { return "", err } @@ -138,7 +139,7 @@ func Activate(sink io.Writer, targetFolder string) error { return err } targetJson := filepath.Join(targetFolder, activateFile) - err = os.WriteFile(targetJson, body, 0o644) + err = pathlib.WriteFile(targetJson, body, 0o644) if err != nil { return err } diff --git a/conda/condayaml.go b/conda/condayaml.go index 67bc5107..d046eb78 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -430,13 +430,13 @@ func (it *Environment) SaveAs(filename string) error { return err } common.Trace("FINAL conda environment file as %v:\n---\n%v---", filename, content) - return os.WriteFile(filename, []byte(content), 0o640) + return pathlib.WriteFile(filename, []byte(content), 0o640) } func (it *Environment) SaveAsRequirements(filename string) error { content := it.AsRequirementsText() common.Trace("FINAL pip requirements as %v:\n---\n%v\n---", filename, content) - return os.WriteFile(filename, []byte(content), 0o640) + return pathlib.WriteFile(filename, []byte(content), 0o640) } func (it *Environment) AsYaml() (string, error) { diff --git a/conda/dependencies.go b/conda/dependencies.go index cd1c7e83..6bd78631 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -11,6 +11,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "gopkg.in/yaml.v2" ) @@ -125,7 +126,7 @@ func goldenMaster(targetFolder string, pipUsed bool) (err error) { fail.On(err != nil, "Failed to make yaml, reason: %v", err) goldenfile := GoldenMasterFilename(targetFolder) common.Debug("%sGolden EE file at: %v%s", pretty.Yellow, goldenfile, pretty.Reset) - return os.WriteFile(goldenfile, body, 0644) + return pathlib.WriteFile(goldenfile, body, 0644) } func LoadWantedDependencies(filename string) dependencies { diff --git a/conda/workflows.go b/conda/workflows.go index 4f611348..86d8e323 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -272,7 +272,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Debug("=== finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") - err = os.WriteFile(markerFile, []byte(yaml), 0o644) + err = pathlib.WriteFile(markerFile, []byte(yaml), 0o644) if err != nil { return false, false } @@ -281,7 +281,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if ok { venvContent := fmt.Sprintf(venvTemplate, targetFolder, pythonVersionAt(targetFolder)) venvFile := filepath.Join(targetFolder, "pyvenv.cfg") - err = os.WriteFile(venvFile, []byte(venvContent), 0o644) + err = pathlib.WriteFile(venvFile, []byte(venvContent), 0o644) if err != nil { return false, false } diff --git a/docs/changelog.md b/docs/changelog.md index 38c01f62..f9b768d7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.5.6 (date: 8.2.2023) + +- bugfix: create missing folders while creating and writing some files +- improvement: added optional top N biggest files sizes on catalog listing + ## v13.5.5 (date: 7.2.2023) - bugfix: contain output of checking long path support on Windows diff --git a/htfs/directory.go b/htfs/directory.go index ada5c67c..8c2ce854 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -15,6 +15,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/set" ) var ( @@ -72,6 +73,24 @@ func NewRoot(path string) (*Root, error) { }, nil } +func (it *Root) Top(count int) map[string]int64 { + target := make(map[string]int64) + it.Tree.fillSizes("", target) + sizes := set.Values(target) + total := len(sizes) + if total > count { + sizes = sizes[total-count:] + } + members := set.Membership(sizes) + result := make(map[string]int64) + for filename, size := range target { + if members[size] { + result[filename] = size + } + } + return result +} + func (it *Root) Show(filename string) ([]byte, error) { return it.Tree.Show(filepath.SplitList(filename), filename) } @@ -163,7 +182,7 @@ func (it *Root) SaveAs(filename string) error { if err != nil { return err } - sink, err := os.Create(filename) + sink, err := pathlib.Create(filename) if err != nil { return err } @@ -225,6 +244,15 @@ func showFile(filename string) (content []byte, err error) { return sink.Bytes(), nil } +func (it *Dir) fillSizes(prefix string, target map[string]int64) { + for filename, file := range it.Files { + target[filepath.Join(prefix, filename)] = file.Size + } + for dirname, dir := range it.Dirs { + dir.fillSizes(filepath.Join(prefix, dirname), target) + } +} + func (it *Dir) Show(path []string, fullpath string) ([]byte, error) { if len(path) > 1 { subtree, ok := it.Dirs[path[0]] diff --git a/htfs/library.go b/htfs/library.go index 1e709a52..cc4610ae 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -165,7 +165,7 @@ func (it *hololib) Export(catalogs, known []string, archive string) (err error) common.TimelineBegin("holotree export start") defer common.TimelineEnd() - handle, err := os.Create(archive) + handle, err := pathlib.Create(archive) fail.On(err != nil, "Could not create archive %q.", archive) writer := zip.NewWriter(handle) defer writer.Close() diff --git a/operations/cache.go b/operations/cache.go index efc34b69..a40a8e8f 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -96,7 +96,7 @@ func (it *Cache) Save() error { } defer locker.Release() - sink, err := os.Create(cacheLocation()) + sink, err := pathlib.Create(cacheLocation()) if err != nil { return err } diff --git a/operations/community.go b/operations/community.go index bd3d29a8..c7a4c39a 100644 --- a/operations/community.go +++ b/operations/community.go @@ -5,11 +5,11 @@ import ( "fmt" "io" "net/http" - "os" "regexp" "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/settings" ) @@ -58,7 +58,7 @@ func DownloadCommunityRobot(url, filename string) error { return fmt.Errorf("%s (%s)", response.Status, url) } - out, err := os.Create(filename) + out, err := pathlib.Create(filename) if err != nil { return err } diff --git a/operations/fixing.go b/operations/fixing.go index 12e86a71..4b23fd10 100644 --- a/operations/fixing.go +++ b/operations/fixing.go @@ -44,7 +44,7 @@ func fixShellFile(fullpath string) { return } common.Debug("Fixing newlines in file: %v", fullpath) - err = os.WriteFile(fullpath, ToUnix(content), 0o755) + err = pathlib.WriteFile(fullpath, ToUnix(content), 0o755) if err != nil { common.Log("Failure %v while fixing newlines in %v!", err, fullpath) } diff --git a/operations/pull.go b/operations/pull.go index 4ae63fc1..1f8334a1 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -89,7 +89,7 @@ func downloadMissingEnvironmentParts(count int, origin, catalogName, selection s fail.On(response.StatusCode < 200 || 299 < response.StatusCode, "%s (%s)", response.Status, url) - out, err := os.Create(filename) + out, err := pathlib.Create(filename) fail.On(err != nil, "Creating temporary file %q failed, reason: %v", filename, err) defer pathlib.TryRemove("temporary", filename) diff --git a/operations/updownload.go b/operations/updownload.go index d874b9e7..71be4959 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -98,7 +98,7 @@ func putContent(client cloud.Client, awsUrl, zipfile string) error { } func getContent(client cloud.Client, awsUrl, zipfile string) error { - handle, err := os.Create(zipfile) + handle, err := pathlib.Create(zipfile) if err != nil { return err } diff --git a/operations/zipper.go b/operations/zipper.go index b7a1e88c..be7060a7 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -51,11 +51,7 @@ func (it *WriteTarget) execute() error { return err } defer source.Close() - err = os.MkdirAll(filepath.Dir(it.Target), 0o750) - if err != nil { - return err - } - target, err := os.Create(it.Target) + target, err := pathlib.Create(it.Target) if err != nil { return err } @@ -240,7 +236,7 @@ type zipper struct { } func newZipper(filename string) (*zipper, error) { - handle, err := os.Create(filename) + handle, err := pathlib.Create(filename) if err != nil { return nil, err } diff --git a/pathlib/functions.go b/pathlib/functions.go index f696c2f3..2bd6d115 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -22,6 +22,22 @@ func TempDir() string { return base } +func Create(filename string) (*os.File, error) { + _, err := EnsureParentDirectory(filename) + if err != nil { + return nil, fmt.Errorf("Failed to ensure that parent directories for %q exist, reason: %v", filename, err) + } + return os.Create(filename) +} + +func WriteFile(filename string, data []byte, mode os.FileMode) error { + _, err := EnsureParentDirectory(filename) + if err != nil { + return fmt.Errorf("Failed to ensure that parent directories for %q exist, reason: %v", filename, err) + } + return os.WriteFile(filename, data, mode) +} + func Exists(pathname string) bool { _, err := os.Stat(pathname) return !os.IsNotExist(err) @@ -92,24 +108,29 @@ func kiloShift(size float64) float64 { return size / 1024.0 } -func HumaneSize(pathname string) string { - rawsize, ok := Size(pathname) - if !ok { - return "N/A" - } +func HumaneSizer(rawsize int64) (float64, string) { kilos := kiloShift(float64(rawsize)) if kilos < 1.0 { - return fmt.Sprintf("%db", rawsize) + return float64(rawsize), "b" } megas := kiloShift(kilos) if megas < 1.0 { - return fmt.Sprintf("%3.1fK", kilos) + return kilos, "K" } gigas := kiloShift(megas) if gigas < 1.0 { - return fmt.Sprintf("%3.1fM", megas) + return megas, "M" } - return fmt.Sprintf("%3.1fG", gigas) + return gigas, "G" +} + +func HumaneSize(pathname string) string { + rawsize, ok := Size(pathname) + if !ok { + return "N/A" + } + value, suffix := HumaneSizer(rawsize) + return fmt.Sprintf("%3.1f%s", value, suffix) } func Modtime(pathname string) (time.Time, error) { @@ -253,6 +274,10 @@ func EnsureDirectory(directory string) (string, error) { return doEnsureDirectory(directory, 0o750) } +func EnsureParentDirectory(resource string) (string, error) { + return doEnsureDirectory(filepath.Dir(resource), 0o750) +} + func RemoveEmptyDirectores(starting string) (err error) { defer fail.Around(&err) diff --git a/pathlib/touch.go b/pathlib/touch.go index 71d53e29..7c965b21 100644 --- a/pathlib/touch.go +++ b/pathlib/touch.go @@ -16,7 +16,7 @@ func TouchWhen(location string, when time.Time) { func ForceTouchWhen(location string, when time.Time) { if !Exists(location) { - err := os.WriteFile(location, []byte{}, 0o644) + err := WriteFile(location, []byte{}, 0o644) if err != nil { common.Debug("Touch/creating file %q failed, reason: %v ... ignored!", location, err) } diff --git a/remotree/delta.go b/remotree/delta.go index b955b4de..22cb799e 100644 --- a/remotree/delta.go +++ b/remotree/delta.go @@ -124,7 +124,7 @@ func exportMissing(catalog string, missing []string) (result string, err error) func exportMissingToFile(catalog string, missing []string, filename string) (err error) { defer fail.Around(&err) - handle, err := os.Create(filename) + handle, err := pathlib.Create(filename) fail.On(err != nil, "Could not create export file %q, reason: %v", filename, err) defer handle.Close() diff --git a/settings/profile.go b/settings/profile.go index 5a5db138..aa0c3fa4 100644 --- a/settings/profile.go +++ b/settings/profile.go @@ -34,7 +34,7 @@ func (it *Profile) SaveAs(filename string) error { if err != nil { return err } - return os.WriteFile(filename, body, 0o666) + return pathlib.WriteFile(filename, body, 0o666) } func (it *Profile) LoadFrom(filename string) error { @@ -94,7 +94,7 @@ func removeIfExists(filename string) error { func saveIfBody(filename string, body []byte) error { if body != nil && len(body) > 0 { - return os.WriteFile(filename, body, 0o666) + return pathlib.WriteFile(filename, body, 0o666) } return nil } diff --git a/shell/task.go b/shell/task.go index f50092b3..4199321d 100644 --- a/shell/task.go +++ b/shell/task.go @@ -9,6 +9,7 @@ import ( "github.com/google/shlex" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" ) type Common interface { @@ -97,12 +98,12 @@ func (it *Task) Tee(folder string, interactive bool) (int, error) { if err != nil { return -600, err } - outfile, err := os.Create(filepath.Join(folder, "stdout.log")) + outfile, err := pathlib.Create(filepath.Join(folder, "stdout.log")) if err != nil { return -601, err } defer outfile.Close() - errfile, err := os.Create(filepath.Join(folder, "stderr.log")) + errfile, err := pathlib.Create(filepath.Join(folder, "stderr.log")) if err != nil { return -602, err } From e2b8a6b90112ab766a51dc58659ee27b8cbcb953 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 10 Feb 2023 08:27:48 +0200 Subject: [PATCH 368/516] BUGFIX: holotree delete slowness (v13.5.7) - bugfix: holotree delete and plan were doing too many calls to find same environments (which mean they were really slow) - some name refactorings to clarify intent of functions --- cmd/holotreeDelete.go | 15 ++++---- cmd/holotreeExport.go | 6 +-- cmd/holotreeList.go | 6 ++- cmd/holotreePlan.go | 25 ++++++------- common/version.go | 2 +- docs/changelog.md | 6 +++ htfs/commands.go | 32 ---------------- htfs/directory.go | 78 ++++++++++++++++++++++++++++++++++++++- htfs/functions.go | 27 +++----------- htfs/library.go | 36 ++---------------- operations/diagnostics.go | 6 +-- pathlib/functions.go | 6 +++ pathlib/walk.go | 2 +- remotree/listings.go | 2 +- 14 files changed, 130 insertions(+), 119 deletions(-) diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go index ebd5e5c0..b179f7ec 100644 --- a/cmd/holotreeDelete.go +++ b/cmd/holotreeDelete.go @@ -9,15 +9,14 @@ import ( ) func deleteByPartialIdentity(partials []string) { - for _, prefix := range partials { - for _, label := range htfs.FindEnvironment(prefix) { - common.Log("Removing %v", label) - if dryFlag { - continue - } - err := htfs.RemoveHolotreeSpace(label) - pretty.Guard(err == nil, 1, "Error: %v", err) + _, roots := htfs.LoadCatalogs() + for _, label := range roots.FindEnvironments(partials) { + common.Log("Removing %v", label) + if dryFlag { + continue } + err := roots.RemoveHolotreeSpace(label) + pretty.Guard(err == nil, 1, "Error: %v", err) } } diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index e3f99f00..bdfb99f6 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -36,12 +36,12 @@ func holotreeExport(catalogs, known []string, archive string) { func listCatalogs(jsonForm bool) { if jsonForm { - nice, err := json.MarshalIndent(htfs.Catalogs(), "", " ") + nice, err := json.MarshalIndent(htfs.CatalogNames(), "", " ") pretty.Guard(err == nil, 2, "%s", err) common.Stdout("%s\n", nice) } else { common.Log("Selectable catalogs (you can use substrings):") - for _, catalog := range htfs.Catalogs() { + for _, catalog := range htfs.CatalogNames() { common.Log("- %s", catalog) } } @@ -49,7 +49,7 @@ func listCatalogs(jsonForm bool) { func selectCatalogs(filters []string) []string { result := make([]string, 0, len(filters)) - for _, catalog := range htfs.Catalogs() { + for _, catalog := range htfs.CatalogNames() { for _, filter := range filters { if strings.Contains(catalog, filter) { result = append(result, catalog) diff --git a/cmd/holotreeList.go b/cmd/holotreeList.go index 5e354e77..c9bb7837 100644 --- a/cmd/holotreeList.go +++ b/cmd/holotreeList.go @@ -17,7 +17,8 @@ func humaneHolotreeSpaceListing() { tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) tabbed.Write([]byte("Identity\tController\tSpace\tBlueprint\tFull path\n")) tabbed.Write([]byte("--------\t----------\t-----\t--------\t---------\n")) - for _, space := range htfs.Spaces() { + _, roots := htfs.LoadCatalogs() + for _, space := range roots.Spaces() { data := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", space.Identity, space.Controller, space.Space, space.Blueprint, space.Path) tabbed.Write([]byte(data)) } @@ -26,7 +27,8 @@ func humaneHolotreeSpaceListing() { func jsonicHolotreeSpaceListing() { details := make(map[string]map[string]string) - for _, space := range htfs.Spaces() { + _, roots := htfs.LoadCatalogs() + for _, space := range roots.Spaces() { hold, ok := details[space.Identity] if !ok { hold = make(map[string]string) diff --git a/cmd/holotreePlan.go b/cmd/holotreePlan.go index e0324de9..81cb3dfd 100644 --- a/cmd/holotreePlan.go +++ b/cmd/holotreePlan.go @@ -19,19 +19,18 @@ var holotreePlanCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { found := false - for _, prefix := range args { - for _, label := range htfs.FindEnvironment(prefix) { - planfile, ok := htfs.InstallationPlan(label) - pretty.Guard(ok, 1, "Could not find plan for: %v", label) - source, err := os.Open(planfile) - pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) - defer source.Close() - analyzer := conda.NewPlanAnalyzer(false) - defer analyzer.Close() - sink := io.MultiWriter(os.Stdout, analyzer) - io.Copy(sink, source) - found = true - } + _, roots := htfs.LoadCatalogs() + for _, label := range roots.FindEnvironments(args) { + planfile, ok := roots.InstallationPlan(label) + pretty.Guard(ok, 1, "Could not find plan for: %v", label) + source, err := os.Open(planfile) + pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) + defer source.Close() + analyzer := conda.NewPlanAnalyzer(false) + defer analyzer.Close() + sink := io.MultiWriter(os.Stdout, analyzer) + io.Copy(sink, source) + found = true } pretty.Guard(found, 3, "Nothing matched given plans!") pretty.Ok() diff --git a/common/version.go b/common/version.go index a023f4d5..88adb669 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.6` + Version = `v13.5.7` ) diff --git a/docs/changelog.md b/docs/changelog.md index f9b768d7..b7ef4213 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v13.5.7 (date: 10.2.2023) + +- bugfix: holotree delete and plan were doing too many calls to find same + environments (which mean they were really slow) +- some name refactorings to clarify intent of functions + ## v13.5.6 (date: 8.2.2023) - bugfix: create missing folders while creating and writing some files diff --git a/htfs/commands.go b/htfs/commands.go index 8fecf66e..a3693d86 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -168,38 +168,6 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec return nil } -func FindEnvironment(fragment string) []string { - result := make([]string, 0, 10) - for directory, _ := range Spacemap() { - name := filepath.Base(directory) - if strings.Contains(name, fragment) { - result = append(result, name) - } - } - return result -} - -func InstallationPlan(hash string) (string, bool) { - finalplan := filepath.Join(common.HolotreeLocation(), hash, "rcc_plan.log") - return finalplan, pathlib.IsFile(finalplan) -} - -func RemoveHolotreeSpace(label string) (err error) { - defer fail.Around(&err) - - for directory, metafile := range Spacemap() { - name := filepath.Base(directory) - if name != label { - continue - } - pathlib.TryRemove("metafile", metafile) - pathlib.TryRemove("lockfile", directory+".lck") - err = pathlib.TryRemoveAll("space", directory) - fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) - } - return nil -} - func RobotBlueprints(userBlueprints []string, packfile string) (robot.Robot, []string) { var err error var config robot.Robot diff --git a/htfs/directory.go b/htfs/directory.go index 8c2ce854..38b2f3cb 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -56,6 +56,81 @@ type Root struct { source string } +type Roots []*Root + +func (it Roots) BaseFolders() []string { + result := []string{} + for _, root := range it { + result = append(result, filepath.Dir(root.Path)) + } + return set.Set(result) +} + +func (it Roots) Spaces() Roots { + roots := make(Roots, 0, 20) + for directory, metafile := range it.Spacemap() { + root, err := NewRoot(directory) + if err != nil { + continue + } + err = root.LoadFrom(metafile) + if err != nil { + continue + } + roots = append(roots, root) + } + return roots +} + +func (it Roots) Spacemap() map[string]string { + result := make(map[string]string) + for _, basedir := range it.BaseFolders() { + for _, metafile := range pathlib.Glob(basedir, "*.meta") { + result[metafile[:len(metafile)-5]] = metafile + } + } + return result +} + +func (it Roots) FindEnvironments(fragments []string) []string { + result := make([]string, 0, 10) + for directory, _ := range it.Spacemap() { + name := filepath.Base(directory) + for _, fragment := range fragments { + if strings.Contains(name, fragment) { + result = append(result, name) + } + } + } + return set.Set(result) +} + +func (it Roots) InstallationPlan(hash string) (string, bool) { + for _, directory := range it.BaseFolders() { + finalplan := filepath.Join(directory, hash, "rcc_plan.log") + if pathlib.IsFile(finalplan) { + return finalplan, true + } + } + return "", false +} + +func (it Roots) RemoveHolotreeSpace(label string) (err error) { + defer fail.Around(&err) + + for directory, metafile := range it.Spacemap() { + name := filepath.Base(directory) + if name != label { + continue + } + pathlib.TryRemove("metafile", metafile) + pathlib.TryRemove("lockfile", directory+".lck") + err = pathlib.TryRemoveAll("space", directory) + fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) + } + return nil +} + func NewRoot(path string) (*Root, error) { fullpath, err := pathlib.Abs(path) if err != nil { @@ -206,8 +281,6 @@ func (it *Root) ReadFrom(source io.Reader) error { } func (it *Root) LoadFrom(filename string) error { - common.TimelineBegin("holotree load %q", filename) - defer common.TimelineEnd() source, err := os.Open(filename) if err != nil { return err @@ -218,6 +291,7 @@ func (it *Root) LoadFrom(filename string) error { return err } it.source = filename + defer common.Timeline("holotree catalog %q loaded", filename) defer reader.Close() return it.ReadFrom(reader) } diff --git a/htfs/functions.go b/htfs/functions.go index 367bda2a..949528e0 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "runtime" - "sort" "sync" "github.com/robocorp/rcc/anywork" @@ -504,8 +503,8 @@ func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { } } -func ignoreFailedCatalogs(suspects []*Root) []*Root { - roots := make([]*Root, 0, len(suspects)) +func ignoreFailedCatalogs(suspects Roots) Roots { + roots := make(Roots, 0, len(suspects)) for _, root := range suspects { if root != nil { roots = append(roots, root) @@ -514,11 +513,11 @@ func ignoreFailedCatalogs(suspects []*Root) []*Root { return roots } -func LoadCatalogs() ([]string, []*Root) { +func LoadCatalogs() ([]string, Roots) { common.TimelineBegin("catalog load start") defer common.TimelineEnd() - catalogs := Catalogs() - roots := make([]*Root, len(catalogs)) + catalogs := CatalogNames() + roots := make(Roots, len(catalogs)) for at, catalog := range catalogs { fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) anywork.Backlog(CatalogLoader(fullpath, at, roots)) @@ -529,21 +528,7 @@ func LoadCatalogs() ([]string, []*Root) { return catalogs, ignoreFailedCatalogs(roots) } -func BaseFolders() []string { - _, roots := LoadCatalogs() - folders := make(map[string]bool) - result := []string{} - for _, root := range roots { - folders[filepath.Dir(root.Path)] = true - } - for folder, _ := range folders { - result = append(result, folder) - } - sort.Strings(result) - return result -} - -func CatalogLoader(catalog string, at int, roots []*Root) anywork.Work { +func CatalogLoader(catalog string, at int, roots Roots) anywork.Work { return func() { tempdir := filepath.Join(common.RobocorpTemp(), "shadow") shadow, err := NewRoot(tempdir) diff --git a/htfs/library.go b/htfs/library.go index cc4610ae..004939d3 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "runtime" - "sort" "strings" "sync" "time" @@ -18,6 +17,7 @@ import ( "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" ) const ( @@ -290,40 +290,12 @@ func (it *hololib) queryBlueprint(key string) bool { return pathlib.IsFile(catalog) } -func Catalogs() []string { +func CatalogNames() []string { result := make([]string, 0, 10) for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*v12.*") { - result = append(result, catalog) + result = append(result, filepath.Base(catalog)) } - sort.Strings(result) - return result -} - -func Spacemap() map[string]string { - result := make(map[string]string) - for _, basedir := range BaseFolders() { - for _, metafile := range pathlib.Glob(basedir, "*.meta") { - fullpath := filepath.Join(basedir, metafile) - result[fullpath[:len(fullpath)-5]] = fullpath - } - } - return result -} - -func Spaces() []*Root { - roots := make([]*Root, 0, 20) - for directory, metafile := range Spacemap() { - root, err := NewRoot(directory) - if err != nil { - continue - } - err = root.LoadFrom(metafile) - if err != nil { - continue - } - roots = append(roots, root) - } - return roots + return set.Set(result) } func ControllerSpaceName(client, tag []byte) string { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 15feaf68..b634d785 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -542,10 +542,10 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str } func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { - jsons := pathlib.Glob(rootdir, "*.json") + jsons := pathlib.RecursiveGlob(rootdir, "*.json") diagnoseFilesUnmarshal(json.Unmarshal, "JSON", rootdir, jsons, target) - yamls := pathlib.Glob(rootdir, "*.yaml") - yamls = append(yamls, pathlib.Glob(rootdir, "*.yml")...) + yamls := pathlib.RecursiveGlob(rootdir, "*.yaml") + yamls = append(yamls, pathlib.RecursiveGlob(rootdir, "*.yml")...) diagnoseFilesUnmarshal(yaml.Unmarshal, "YAML", rootdir, yamls, target) } diff --git a/pathlib/functions.go b/pathlib/functions.go index 2bd6d115..150fbfa6 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -38,6 +38,12 @@ func WriteFile(filename string, data []byte, mode os.FileMode) error { return os.WriteFile(filename, data, mode) } +func Glob(directory string, pattern string) []string { + fullpath := filepath.Join(directory, pattern) + result, _ := filepath.Glob(fullpath) + return result +} + func Exists(pathname string) bool { _, err := os.Stat(pathname) return !os.IsNotExist(err) diff --git a/pathlib/walk.go b/pathlib/walk.go index d95a245e..a435214e 100644 --- a/pathlib/walk.go +++ b/pathlib/walk.go @@ -205,7 +205,7 @@ func Walk(directory string, ignore Ignore, report Report) error { return ForceWalk(directory, ForceNothing, ignore, report) } -func Glob(directory string, pattern string) []string { +func RecursiveGlob(directory string, pattern string) []string { result := []string{} ignore := func(entry os.FileInfo) bool { match, err := filepath.Match(pattern, entry.Name()) diff --git a/remotree/listings.go b/remotree/listings.go index dde911c5..b44e8f01 100644 --- a/remotree/listings.go +++ b/remotree/listings.go @@ -65,7 +65,7 @@ func loadSingleCatalog(catalog string) (root *htfs.Root, err error) { } func loadCatalogParts(catalog string) (string, bool) { - catalogs := htfs.Catalogs() + catalogs := htfs.CatalogNames() if !set.Member(catalogs, catalog) { return "", false } From 6be96a12d3b8abe7ba565c3b0639ad89defa2f62 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 10 Feb 2023 09:58:56 +0200 Subject: [PATCH 369/516] BUGFIX: holotree delete defaults (v13.5.8) - bugfix: holotree delete --space option was always set --- cmd/holotreeDelete.go | 21 +++++++++++++++------ common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/directory.go | 3 ++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go index b179f7ec..c04a15c6 100644 --- a/cmd/holotreeDelete.go +++ b/cmd/holotreeDelete.go @@ -8,10 +8,18 @@ import ( "github.com/spf13/cobra" ) +var ( + deleteSpace string +) + func deleteByPartialIdentity(partials []string) { _, roots := htfs.LoadCatalogs() + var note string + if dryFlag { + note = "[dry run] " + } for _, label := range roots.FindEnvironments(partials) { - common.Log("Removing %v", label) + common.Log("%sRemoving %v", note, label) if dryFlag { continue } @@ -22,24 +30,25 @@ func deleteByPartialIdentity(partials []string) { var holotreeDeleteCmd = &cobra.Command{ Use: "delete *", - Short: "Delete holotree controller space.", - Long: "Delete holotree controller space.", + Short: "Delete one or more holotree controller spaces.", + Long: "Delete one or more holotree controller spaces.", Aliases: []string{"del"}, Run: func(cmd *cobra.Command, args []string) { partials := make([]string, 0, len(args)+1) if len(args) > 0 { partials = append(partials, args...) } - if len(common.HolotreeSpace) > 0 { - partials = append(partials, htfs.ControllerSpaceName([]byte(common.ControllerIdentity()), []byte(common.HolotreeSpace))) + if len(deleteSpace) > 0 { + partials = append(partials, htfs.ControllerSpaceName([]byte(common.ControllerIdentity()), []byte(deleteSpace))) } pretty.Guard(len(partials) > 0, 1, "Must provide either --space flag, or partial environment identity!") deleteByPartialIdentity(partials) + pretty.Ok() }, } func init() { holotreeCmd.AddCommand(holotreeDeleteCmd) holotreeDeleteCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") - holotreeDeleteCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify environment to delete.") + holotreeDeleteCmd.Flags().StringVarP(&deleteSpace, "space", "s", "", "Client specific name to identify environment to delete.") } diff --git a/common/version.go b/common/version.go index 88adb669..b2666bc1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.7` + Version = `v13.5.8` ) diff --git a/docs/changelog.md b/docs/changelog.md index b7ef4213..1eeda679 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.5.8 (date: 10.2.2023) + +- bugfix: holotree delete --space option was always set + ## v13.5.7 (date: 10.2.2023) - bugfix: holotree delete and plan were doing too many calls to find same diff --git a/htfs/directory.go b/htfs/directory.go index 38b2f3cb..9fd443cf 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -126,7 +126,8 @@ func (it Roots) RemoveHolotreeSpace(label string) (err error) { pathlib.TryRemove("metafile", metafile) pathlib.TryRemove("lockfile", directory+".lck") err = pathlib.TryRemoveAll("space", directory) - fail.On(err != nil, "Problem removing %q, reason: %s.", directory, err) + fail.On(err != nil, "Problem removing %q, reason: %v.", directory, err) + common.Timeline("removed holotree space %q", directory) } return nil } From 673e65f715d69c18c72454e47334df4e0ff0dbf4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 10 Feb 2023 11:37:05 +0200 Subject: [PATCH 370/516] UPGRADE: github actions and go upgrades (v13.6.0) --- .github/workflows/rcc.yaml | 12 ++++++------ common/version.go | 2 +- docs/changelog.md | 4 ++++ hamlet/hamlet.go | 1 - 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index c62cbf06..5d04077f 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -13,9 +13,9 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: - go-version: '1.18.x' + go-version: '1.19.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' @@ -33,15 +33,15 @@ jobs: matrix: os: ['ubuntu'] steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: - go-version: '1.18.x' + go-version: '1.19.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.9' - uses: actions/checkout@v1 - name: Setup run: rake robotsetup diff --git a/common/version.go b/common/version.go index b2666bc1..9dae4296 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.5.8` + Version = `v13.6.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1eeda679..e216f61c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.6.0 (date: 10.2.2023) + +- upgrade: upgrading github actions and also using newer golang and python there + ## v13.5.8 (date: 10.2.2023) - bugfix: holotree delete --space option was always set diff --git a/hamlet/hamlet.go b/hamlet/hamlet.go index 7cdba2f6..94c649bb 100644 --- a/hamlet/hamlet.go +++ b/hamlet/hamlet.go @@ -29,7 +29,6 @@ when used in code. I like my specifications short and declarative, not longwindy and procedural code form. One line, one expectation! - */ package hamlet From d7607e7bb2d88ba3b5a1de3b579ad769a1aef129 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 15 Feb 2023 15:13:15 +0200 Subject: [PATCH 371/516] EXPERIMENT: maintenance by chance (v13.6.1) - experiment: using probability to run some of maintenance functions and making rcc little bit faster depending on chance --- cmd/rcc/main.go | 8 ++++++-- common/algorithms.go | 8 ++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ xviper/wrapper.go | 6 +++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 612826bf..fc8c51b7 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -101,13 +101,17 @@ func main() { common.TimelineBegin("Start [private mode]. (parent/pid: %d/%d)", os.Getppid(), os.Getpid()) } defer common.EndOfTimeline() - go startTempRecycling() + if common.OneOutOf(6) { + go startTempRecycling() + } defer markTempForRecycling() defer os.Stderr.Sync() defer os.Stdout.Sync() cmd.Execute() common.Timeline("Command execution done.") - TimezoneMetric() + if common.OneOutOf(5) { + TimezoneMetric() + } anywork.Sync() } diff --git a/common/algorithms.go b/common/algorithms.go index 4904c97c..d383574d 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "fmt" "math" + "math/rand" "time" "github.com/dchest/siphash" @@ -56,3 +57,10 @@ func DayCountSince(timestamp time.Time) int { days := math.Floor(duration.Hours() / 24.0) return int(days) } + +func OneOutOf(limit uint8) bool { + if limit > 1 { + return rand.Intn(int(limit)) == 0 + } + return true +} diff --git a/common/version.go b/common/version.go index 9dae4296..df5fb647 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.0` + Version = `v13.6.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index e216f61c..3074c1b0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.6.1 (date: 15.2.2023) + +- experiment: using probability to run some of maintenance functions and + making rcc little bit faster depending on chance + ## v13.6.0 (date: 10.2.2023) - upgrade: upgrading github actions and also using newer golang and python there diff --git a/xviper/wrapper.go b/xviper/wrapper.go index a93f73e9..5622d89a 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -58,7 +58,7 @@ func (it *config) Save() { } } -func (it *config) Reload() { +func (it *config) reload() { completed := pathlib.LockWaitMessage(it.Lockfile, "Serialized config access [config lock]") locker, err := pathlib.Locker(it.Lockfile, 125) completed() @@ -85,7 +85,7 @@ func (it *config) Reload() { func (it *config) Reset(filename string) { it.Filename = filename it.Lockfile = fmt.Sprintf("%s.lck", filename) - it.Reload() + it.reload() } func (it *config) Summon() *viper.Viper { @@ -98,7 +98,7 @@ func (it *config) Summon() *viper.Viper { } if when.After(it.Timestamp) { common.Debug("Configuration %v changed, reloading!", it.Filename) - it.Reload() + it.reload() } return it.Viper } From edde51b2d455d8ab1940f18e3544d2419a161260 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 16 Feb 2023 13:56:25 +0200 Subject: [PATCH 372/516] BUGFIX: WaitGroup problem (v13.6.2) - bugfix: changed WaitGroup to WorkGroup (self implemented work synchronization) --- anywork/pending.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++ anywork/worker.go | 9 +++--- common/version.go | 2 +- docs/changelog.md | 4 +++ 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 anywork/pending.go diff --git a/anywork/pending.go b/anywork/pending.go new file mode 100644 index 00000000..b0e9c771 --- /dev/null +++ b/anywork/pending.go @@ -0,0 +1,74 @@ +package anywork + +type ( + WorkGroup interface { + add() + done() + Wait() + } + + workgroup struct { + level waitpipe + waiting waiting + } + + waitpipe chan bool + waiting chan waitpipe +) + +func NewGroup() WorkGroup { + group := &workgroup{ + level: make(waitpipe, 5), + waiting: make(waiting, 5), + } + go group.waiter() + return group +} + +func (it *workgroup) add() { + it.level <- true +} + +func (it *workgroup) done() { + it.level <- false +} + +func (it *workgroup) Wait() { + reply := make(waitpipe) + it.waiting <- reply + _, _ = <-reply +} + +func (it *workgroup) waiter() { + var counter int64 + pending := make([]waitpipe, 0, 5) +forever: + for { + if counter < 0 { + panic("anywork: counter below zero") + } + if counter == 0 && len(pending) > 0 { + for _, waiter := range pending { + close(waiter) + } + pending = make([]waitpipe, 0, 5) + } + select { + case up, ok := <-it.level: + if !ok { + break forever + } + if up { + counter += 1 + } else { + counter -= 1 + } + case waiter, ok := <-it.waiting: + if !ok { + break forever + } + pending = append(pending, waiter) + } + } + panic("anywork: for some reason, waiter have just exited") +} diff --git a/anywork/worker.go b/anywork/worker.go index c9a529e2..33294ce9 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -5,11 +5,10 @@ import ( "io" "os" "runtime" - "sync" ) var ( - group *sync.WaitGroup + group WorkGroup pipeline WorkQueue failpipe Failures errcount Counters @@ -30,7 +29,6 @@ func catcher(title string, identity uint64) { } func process(fun Work, identity uint64) { - defer group.Done() defer catcher("process", identity) fun() } @@ -43,6 +41,7 @@ func member(identity uint64) { break } process(work, identity) + group.done() } } @@ -60,7 +59,7 @@ func watcher(failures Failures, counters Counters) { } func init() { - group = &sync.WaitGroup{} + group = NewGroup() pipeline = make(WorkQueue, 100000) failpipe = make(Failures) errcount = make(Counters) @@ -92,7 +91,7 @@ func AutoScale() { func Backlog(todo Work) { if todo != nil { - group.Add(1) + group.add() pipeline <- todo } } diff --git a/common/version.go b/common/version.go index df5fb647..8b59ec14 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.1` + Version = `v13.6.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3074c1b0..e3dddcdc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.6.2 (date: 16.2.2023) UNSTABLE + +- bugfix: changed WaitGroup to WorkGroup (self implemented work synchronization) + ## v13.6.1 (date: 15.2.2023) - experiment: using probability to run some of maintenance functions and From ab2aa611a47c626bbbea0e5760fc991057f6817f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 20 Feb 2023 08:45:13 +0200 Subject: [PATCH 373/516] CHANGE: WorkGroup more deterministic (v13.6.3) - change: changed WorkGroup to not use buffers on incoming messages, since it will be more deterministic --- anywork/pending.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/anywork/pending.go b/anywork/pending.go index b0e9c771..a677fdd5 100644 --- a/anywork/pending.go +++ b/anywork/pending.go @@ -18,8 +18,8 @@ type ( func NewGroup() WorkGroup { group := &workgroup{ - level: make(waitpipe, 5), - waiting: make(waiting, 5), + level: make(waitpipe), + waiting: make(waiting), } go group.waiter() return group diff --git a/common/version.go b/common/version.go index 8b59ec14..5133da88 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.2` + Version = `v13.6.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index e3dddcdc..4421034d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.6.3 (date: 20.2.2023) + +- change: changed WorkGroup to not use buffers on incoming messages, since it + will be more deterministic + ## v13.6.2 (date: 16.2.2023) UNSTABLE - bugfix: changed WaitGroup to WorkGroup (self implemented work synchronization) From df12f9f250ba69913ba2dd17cbd4fcc45245d2b3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 23 Feb 2023 10:41:59 +0200 Subject: [PATCH 374/516] DOCS: documentation update (v13.6.4) - documentation updates on netdiagnostics and troubleshooting --- common/version.go | 2 +- docs/README.md | 73 +++++++++++++++++++++-------------------- docs/changelog.md | 4 +++ docs/recipes.md | 25 ++++++++++++++ docs/troubleshooting.md | 21 +++++++++++- 5 files changed, 88 insertions(+), 37 deletions(-) diff --git a/common/version.go b/common/version.go index 5133da88..fe010a76 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.3` + Version = `v13.6.4` ) diff --git a/docs/README.md b/docs/README.md index 69a6cbf6..4fb2e2ea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,39 +33,41 @@ ### 3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) ### 3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) #### 3.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### 3.13 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) -#### 3.13.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.13.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### 3.13.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.13.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) -#### 3.13.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.13.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.13.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.13.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.13.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.13.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.13.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### 3.14 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +### 3.13 [Advanced network diagnostics](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#advanced-network-diagnostics) +#### 3.13.1 [Configuration](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#configuration) +### 3.14 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) #### 3.14.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.14.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) -#### 3.14.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### 3.14.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### 3.14.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.15 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-cicd-pipeline-integration-with-rcc) -#### 3.15.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) -#### 3.15.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) -#### 3.15.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) -#### 3.15.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) -### 3.16 [How to setup custom templates?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-custom-templates) -#### 3.16.1 [Custom template configuration in `settings.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-in-settingsyaml-) -#### 3.16.2 [Custom template configuration file as `templates.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-file-as-templatesyaml-) -#### 3.16.3 [Custom template content in `templates.zip` file.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-content-in-templateszip-file) -#### 3.16.4 [Shared using `https:` protocol ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#shared-using-https-protocol-) -### 3.17 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.18 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.18.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.18.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.19 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +#### 3.14.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### 3.14.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.14.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.14.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.14.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.14.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.14.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.14.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.14.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.14.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### 3.15 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### 3.15.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.15.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### 3.15.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### 3.15.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### 3.15.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### 3.16 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-cicd-pipeline-integration-with-rcc) +#### 3.16.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) +#### 3.16.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) +#### 3.16.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) +#### 3.16.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) +### 3.17 [How to setup custom templates?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-custom-templates) +#### 3.17.1 [Custom template configuration in `settings.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-in-settingsyaml-) +#### 3.17.2 [Custom template configuration file as `templates.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-file-as-templatesyaml-) +#### 3.17.3 [Custom template content in `templates.zip` file.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-content-in-templateszip-file) +#### 3.17.4 [Shared using `https:` protocol ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#shared-using-https-protocol-) +### 3.18 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.19 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.19.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.19.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.20 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) @@ -84,9 +86,10 @@ ### 5.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) ### 5.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) ### 5.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) -### 5.4 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) -#### 5.4.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) -#### 5.4.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) +### 5.4 [Network access related troubleshooting questions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#network-access-related-troubleshooting-questions) +### 5.5 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) +#### 5.5.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) +#### 5.5.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) ## 6 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) ### 6.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) ### 6.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) diff --git a/docs/changelog.md b/docs/changelog.md index 4421034d..5c543a57 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.6.4 (date: 23.2.2023) DOCUMENTATION + +- documentation updates on netdiagnostics and troubleshooting + ## v13.6.3 (date: 20.2.2023) - change: changed WorkGroup to not use buffers on incoming messages, since it diff --git a/docs/recipes.md b/docs/recipes.md index 397b1777..b9855543 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -440,6 +440,31 @@ rcc configure speedtest - generic flag `--trace` shows more verbose debugging messages during execution - flag `--timeline` can be used to see execution timeline and where time was spent +## Advanced network diagnostics + +When using custom endpoints or just needing more control over what network +checks are done, command `rcc configure netdiagnostics` may become helpful. + +```sh +# to test advanced network diagnostics with defaults +rcc configure netdiagnostics + +# to capture advanced network diagnostics defaults to new configuration file +rcc configure netdiagnostics --show > path/to/modified.yaml + +# to test advanced network diagnostics with custom tests +rcc configure netdiagnostics --checks path/to/modified.yaml +``` + +### Configuration + +- get example configuration out using `--show` option (as seen above) +- configuration file format is YAML +- add or remove points to DNS, HTTP HEAD and GET methods +- `url:` and `codes:` are required fields for HEAD and GET checks +- `codes:` field is list of acceptable HTTP response codes +- `content-sha256` is optional, and provides additional confidence when content + is static and result content hash can be calculated (using sha256 algorithm) ## What is in `robot.yaml`? diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1d5d3c2e..6c7583e7 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -5,7 +5,7 @@ ## Tools to help with troubleshooting issues - run command `rcc configuration diagnostics` and see if there are warnings - or errors in output + or errors in output (and same with `rcc configuration netdiagnostics`) - if failure is with specific robot, then try running command `rcc configuration diagnostics --robot path/to/robot.yaml` and see if those robot diagnostics have something that identifies a problem @@ -45,6 +45,25 @@ - you should share your code, or minimal sample code, that can reproduce problem you are having +## Network access related troubleshooting questions + +- are you behind proxy, firewall, VPN, endpoint security solutions, or any + combination of those? +- if you are, do you know, what brand are those products, and if they are + provided by third party service providers, who are those third parties? +- are all those services configured to allow access to essential network places + so that they don't cause interference on cloud access (change request or + response headers, filter out URL parameters, change request or response + bodies, etc.)? +- if those services require additional configuration in robot running machine, + are those configurations in place in profiles used by rcc (service URLs, + usernames and passwords, custom certificates, etc.)? +- if profile is in place, is that specific user account switched to use that + profile? +- are there errors or warnings on `rcc configuration diagnostics` or in + `rcc configuration netdiagnostics` runs? +- does `rcc configuration speedtest` work, and how does performance look like? + ## Known solutions ### Access denied while building holotree environment (Windows) From 7df0773d03cb1949be526183fd0caabefca10b21 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 23 Feb 2023 20:37:08 +0200 Subject: [PATCH 375/516] SECURITY: dependabot flagged (v13.6.5) - dependabot raised update on golang.org/x/text module (upgraded) - security related dependency upgrade --- common/version.go | 2 +- docs/changelog.md | 5 +++++ go.mod | 4 ++-- go.sum | 8 ++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index fe010a76..185bd5d8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.4` + Version = `v13.6.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5c543a57..7e88f4cc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.6.5 (date: 23.2.2023) + +- dependabot raised update on golang.org/x/text module (upgraded) +- security related dependency upgrade + ## v13.6.4 (date: 23.2.2023) DOCUMENTATION - documentation updates on netdiagnostics and troubleshooting diff --git a/go.mod b/go.mod index 0af70013..da896b64 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/mattn/go-isatty v0.0.14 github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.13.0 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 @@ -28,7 +28,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.1 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.3.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9f25e978..ff1044d0 100644 --- a/go.sum +++ b/go.sum @@ -317,8 +317,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -328,8 +328,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 6fe90e9874be1e00d0099cf5b44a6a55a1d756e9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 27 Feb 2023 10:41:19 +0200 Subject: [PATCH 376/516] FEATURE: troubleshooting guides (v13.7.0) - troubleshooting documentation added as `rcc man troubleshooting` command - consolidated and streamlined documentation commands into fewer source files - added robot tests for documentation commands --- cmd/changelog.go | 26 ----------- cmd/features.go | 25 ---------- cmd/license.go | 25 ---------- cmd/man.go | 78 +++++++++++++++++++++++++++++++ cmd/profiles.go | 25 ---------- cmd/recipes.go | 26 ----------- cmd/tutorial.go | 27 ----------- cmd/usecases.go | 25 ---------- common/version.go | 2 +- docs/changelog.md | 6 +++ robot_tests/documentation.robot | 22 +++++++++ robot_tests/unmanaged_space.robot | 1 - 12 files changed, 107 insertions(+), 181 deletions(-) delete mode 100644 cmd/changelog.go delete mode 100644 cmd/features.go delete mode 100644 cmd/license.go delete mode 100644 cmd/profiles.go delete mode 100644 cmd/recipes.go delete mode 100644 cmd/tutorial.go delete mode 100644 cmd/usecases.go create mode 100644 robot_tests/documentation.robot diff --git a/cmd/changelog.go b/cmd/changelog.go deleted file mode 100644 index 51131d61..00000000 --- a/cmd/changelog.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var changelogCmd = &cobra.Command{ - Use: "changelog", - Short: "Show the rcc changelog.", - Long: "Show the rcc changelog.", - Aliases: []string{"changes"}, - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("docs/changelog.md") - if err != nil { - pretty.Exit(1, "Cannot show changelog.md, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(changelogCmd) -} diff --git a/cmd/features.go b/cmd/features.go deleted file mode 100644 index 01d72d1a..00000000 --- a/cmd/features.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var featuresCmd = &cobra.Command{ - Use: "features", - Short: "Show some of rcc features.", - Long: "Show some of rcc features.", - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("docs/features.md") - if err != nil { - pretty.Exit(1, "Cannot show features.md, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(featuresCmd) -} diff --git a/cmd/license.go b/cmd/license.go deleted file mode 100644 index 6dc29518..00000000 --- a/cmd/license.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var licenseCmd = &cobra.Command{ - Use: "license", - Short: "Show the rcc License.", - Long: "Show the rcc License.", - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("assets/man/LICENSE.txt") - if err != nil { - pretty.Exit(1, "Cannot show LICENSE, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(licenseCmd) -} diff --git a/cmd/man.go b/cmd/man.go index 89f379c8..f05bba2b 100644 --- a/cmd/man.go +++ b/cmd/man.go @@ -1,9 +1,15 @@ package cmd import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) +type ( + cobraCommand func(*cobra.Command, []string) +) + var manCmd = &cobra.Command{ Use: "man", Aliases: []string{"manuals", "docs", "doc", "guides", "guide", "m"}, @@ -13,4 +19,76 @@ var manCmd = &cobra.Command{ func init() { rootCmd.AddCommand(manCmd) + + manCmd.AddCommand(&cobra.Command{ + Use: "changelog", + Short: "Show the rcc changelog.", + Long: "Show the rcc changelog.", + Aliases: []string{"changes"}, + Run: makeShowDoc("changelog", "docs/changelog.md"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "features", + Short: "Show some of rcc features.", + Long: "Show some of rcc features.", + Run: makeShowDoc("features", "docs/features.md"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "license", + Short: "Show the rcc License.", + Long: "Show the rcc License.", + Run: makeShowDoc("LICENSE", "assets/man/LICENSE.txt"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "profiles", + Short: "Show configuration profiles documentation.", + Long: "Show configuration profiles documentation.", + Run: makeShowDoc("profile documentation", "docs/profile_configuration.md"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "recipes", + Short: "Show rcc recipes, tips, and tricks.", + Long: "Show rcc recipes, tips, and tricks.", + Aliases: []string{"recipe", "tips", "tricks"}, + Run: makeShowDoc("recipes", "docs/recipes.md"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "troubleshooting", + Short: "Show the rcc troubleshooting documentation.", + Long: "Show the rcc troubleshooting documentation.", + Run: makeShowDoc("troubleshooting", "docs/troubleshooting.md"), + }) + + manCmd.AddCommand(&cobra.Command{ + Use: "usecases", + Short: "Show some of rcc use cases.", + Long: "Show some of rcc use cases.", + Run: makeShowDoc("use-cases", "docs/usecases.md"), + }) + + tutorial := &cobra.Command{ + Use: "tutorial", + Short: "Show the rcc tutorial.", + Long: "Show the rcc tutorial.", + Aliases: []string{"tut"}, + Run: makeShowDoc("tutorial", "assets/man/tutorial.txt"), + } + + manCmd.AddCommand(tutorial) + rootCmd.AddCommand(tutorial) +} + +func makeShowDoc(label, asset string) cobraCommand { + return func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset(asset) + if err != nil { + pretty.Exit(1, "Cannot show %s documentation, reason: %v", label, err) + } + pretty.Page(content) + } } diff --git a/cmd/profiles.go b/cmd/profiles.go deleted file mode 100644 index 2c5ec206..00000000 --- a/cmd/profiles.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var manProfilesCmd = &cobra.Command{ - Use: "profiles", - Short: "Show configuration profiles documentation.", - Long: "Show configuration profiles documentation.", - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("docs/profile_configuration.md") - if err != nil { - pretty.Exit(1, "Cannot show profile_configuration.md, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(manProfilesCmd) -} diff --git a/cmd/recipes.go b/cmd/recipes.go deleted file mode 100644 index 9cf7522a..00000000 --- a/cmd/recipes.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var recipesCmd = &cobra.Command{ - Use: "recipes", - Short: "Show rcc recipes, tips, and tricks.", - Long: "Show rcc recipes, tips, and tricks.", - Aliases: []string{"recipe", "tips", "tricks"}, - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("docs/recipes.md") - if err != nil { - pretty.Exit(1, "Cannot show recipes.md, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(recipesCmd) -} diff --git a/cmd/tutorial.go b/cmd/tutorial.go deleted file mode 100644 index 9e34db24..00000000 --- a/cmd/tutorial.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var tutorialCmd = &cobra.Command{ - Use: "tutorial", - Short: "Show the rcc tutorial.", - Long: "Show the rcc tutorial.", - Aliases: []string{"tut"}, - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("assets/man/tutorial.txt") - if err != nil { - pretty.Exit(1, "Cannot show tutorial text, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(tutorialCmd) - rootCmd.AddCommand(tutorialCmd) -} diff --git a/cmd/usecases.go b/cmd/usecases.go deleted file mode 100644 index 97534f90..00000000 --- a/cmd/usecases.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var usecasesCmd = &cobra.Command{ - Use: "usecases", - Short: "Show some of rcc use cases.", - Long: "Show some of rcc use cases.", - Run: func(cmd *cobra.Command, args []string) { - content, err := blobs.Asset("docs/usecases.md") - if err != nil { - pretty.Exit(1, "Cannot show usecases.md, reason: %v", err) - } - pretty.Page(content) - }, -} - -func init() { - manCmd.AddCommand(usecasesCmd) -} diff --git a/common/version.go b/common/version.go index 185bd5d8..3d107b5a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.6.5` + Version = `v13.7.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7e88f4cc..a197a4a0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v13.7.0 (date: 27.2.2023) + +- troubleshooting documentation added as `rcc man troubleshooting` command +- consolidated and streamlined documentation commands into fewer source files +- added robot tests for documentation commands + ## v13.6.5 (date: 23.2.2023) - dependabot raised update on golang.org/x/text module (upgraded) diff --git a/robot_tests/documentation.robot b/robot_tests/documentation.robot new file mode 100644 index 00000000..943efa40 --- /dev/null +++ b/robot_tests/documentation.robot @@ -0,0 +1,22 @@ +*** Settings *** +Resource resources.robot +Test template Verify documentation +Default tags WIP + +*** Test cases *** DOCUMENTATION EXPECT + +Changelog in documentation changelog troubleshooting documentation added as +Features in documentation features Incomplete list of rcc features +LICENSE in documentation license TERMS AND CONDITIONS FOR USE +Profiles in documentation profiles Profile is way to capture +Recipes in documentation recipes Tips, tricks, and recipies +Troubleshooting in documentation troubleshooting Troubleshooting guidelines and known solutions +Tutorial in documentation tutorial Welcome to RCC tutorial +Use-cases in documentation usecases Incomplete list of rcc use cases + +*** Keywords *** + +Verify documentation + [Arguments] ${document} ${expected} + Step build/rcc man ${document} --controller citests + Must Have ${expected} diff --git a/robot_tests/unmanaged_space.robot b/robot_tests/unmanaged_space.robot index 244356fe..891bb550 100644 --- a/robot_tests/unmanaged_space.robot +++ b/robot_tests/unmanaged_space.robot @@ -3,7 +3,6 @@ Library OperatingSystem Library supporting.py Resource resources.robot Suite Setup Holotree setup -Default tags WIP *** Keywords *** Holotree setup From 5b58b3daf8206bdc9460298a8a9e1be9110da6ab Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 27 Feb 2023 12:05:21 +0200 Subject: [PATCH 377/516] IMPROVEMENT: added missing RCC_REMOTE_AUTHORIZATION (v13.7.1) - added missing `RCC_REMOTE_AUTHORIZATION` variable handling to rcc and passing that variable to rccremote on pull requests --- common/variables.go | 6 ++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/pull.go | 9 +++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/common/variables.go b/common/variables.go index 1a2d9a9b..75be8074 100644 --- a/common/variables.go +++ b/common/variables.go @@ -13,6 +13,7 @@ import ( const ( ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` RCC_REMOTE_ORIGIN = `RCC_REMOTE_ORIGIN` + RCC_REMOTE_AUTHORIZATION = `RCC_REMOTE_AUTHORIZATION` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` ) @@ -84,6 +85,11 @@ func RccRemoteOrigin() string { return os.Getenv(RCC_REMOTE_ORIGIN) } +func RccRemoteAuthorization() (string, bool) { + result := os.Getenv(RCC_REMOTE_AUTHORIZATION) + return result, len(result) > 0 +} + func RobocorpLock() string { return filepath.Join(RobocorpHome(), "robocorp.lck") } diff --git a/common/version.go b/common/version.go index 3d107b5a..979d4113 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.7.0` + Version = `v13.7.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index a197a4a0..aad46819 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.7.1 (date: 27.2.2023) + +- added missing `RCC_REMOTE_AUTHORIZATION` variable handling to rcc and passing + that variable to rccremote on pull requests + ## v13.7.0 (date: 27.2.2023) - troubleshooting documentation added as `rcc man troubleshooting` command diff --git a/operations/pull.go b/operations/pull.go index 1f8334a1..34a5d232 100644 --- a/operations/pull.go +++ b/operations/pull.go @@ -22,6 +22,7 @@ import ( const ( X_RCC_RANDOM_IDENTITY = `X-Rcc-Random-Identity` + AUTHORIZATION = "Authorization" ) func pullOriginFingerprints(origin, catalogName string) (fingerprints string, count int, err error) { @@ -36,6 +37,10 @@ func pullOriginFingerprints(origin, catalogName string) (fingerprints string, co url := fmt.Sprintf("%s/parts/%s", origin, catalogName) request := client.NewRequest(fmt.Sprintf("/parts/%s", catalogName)) request.Headers[X_RCC_RANDOM_IDENTITY] = common.RandomIdentifier() + authorization, ok := common.RccRemoteAuthorization() + if ok { + request.Headers[AUTHORIZATION] = authorization + } response := client.Get(request) common.Timeline("status %d from GET %q", response.Status, url) @@ -80,6 +85,10 @@ func downloadMissingEnvironmentParts(count int, origin, catalogName, selection s request.Header.Add("robocorp-installation-id", xviper.TrackingIdentity()) request.Header.Add("User-Agent", common.UserAgent()) request.Header.Add(X_RCC_RANDOM_IDENTITY, common.RandomIdentifier()) + authorization, ok := common.RccRemoteAuthorization() + if ok { + request.Header.Add(AUTHORIZATION, authorization) + } response, err := client.Do(request) fail.On(err != nil, "Web request to %q failed, reason: %v", url, err) From 0d0e3ac155f409a304c46f9358a360f3ac64fae9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Mar 2023 11:15:47 +0200 Subject: [PATCH 378/516] FEATURE: direct export from prebuild (v13.8.0) - new `--export` option to `rcc holotree prebuild` command, to enable direct export to given hololib.zip filename of new, successfully build catalogs - bugfix: catalog was exported before its content, which would make it so, that catalog is present before its parts --- cmd/holotreePrebuild.go | 10 +++++++++ common/variables.go | 45 +++++++++++++++++++++-------------------- common/version.go | 2 +- docs/changelog.md | 7 +++++++ htfs/commands.go | 3 ++- htfs/library.go | 10 +++++---- 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index 1891aeda..2b00c08d 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -18,6 +18,7 @@ import ( var ( metafileFlag bool forceBuild bool + exportFile string ) func conditionalExpand(filename string) string { @@ -109,14 +110,22 @@ var holotreePrebuildCmd = &cobra.Command{ configurations := metafileExpansion(args, metafileFlag) total, failed := len(configurations), 0 + success := make([]string, 0, total) for at, configfile := range configurations { pretty.Note("%d/%d: Now building config %q", at+1, total, configfile) _, _, err := htfs.NewEnvironment(configfile, "", false, forceBuild, operations.PullCatalog) if err != nil { failed += 1 pretty.Warning("%d/%d: Holotree recording error: %v", at+1, total, err) + } else { + if common.FreshlyBuildEnvironment { + success = append(success, htfs.CatalogName(common.EnvironmentHash)) + } } } + if len(exportFile) > 0 && len(success) > 0 { + holotreeExport(selectCatalogs(success), nil, exportFile) + } pretty.Guard(failed == 0, 2, "%d out of %d environment builds failed! See output above for details.", failed, total) pretty.Ok() }, @@ -126,4 +135,5 @@ func init() { holotreeCmd.AddCommand(holotreePrebuildCmd) holotreePrebuildCmd.Flags().BoolVarP(&metafileFlag, "metafile", "m", false, "Input arguments are actually files containing links/filenames of environment descriptors.") holotreePrebuildCmd.Flags().BoolVarP(&forceBuild, "force", "f", false, "Force environment builds, even when blueprint is already present.") + holotreePrebuildCmd.Flags().StringVarP(&exportFile, "export", "e", "", "Optional filename to export new, successfully build catalogs.") } diff --git a/common/variables.go b/common/variables.go index 75be8074..4a0b0d50 100644 --- a/common/variables.go +++ b/common/variables.go @@ -19,28 +19,29 @@ const ( ) var ( - NoBuild bool - Silent bool - DebugFlag bool - TraceFlag bool - DeveloperFlag bool - StrictFlag bool - SharedHolotree bool - LogLinenumbers bool - NoCache bool - NoOutputCapture bool - Liveonly bool - UnmanagedSpace bool - StageFolder string - ControllerType string - HolotreeSpace string - EnvironmentHash string - SemanticTag string - ForcedRobocorpHome string - When int64 - ProgressMark time.Time - Clock *stopwatch - randomIdentifier string + NoBuild bool + Silent bool + DebugFlag bool + TraceFlag bool + DeveloperFlag bool + StrictFlag bool + SharedHolotree bool + LogLinenumbers bool + NoCache bool + NoOutputCapture bool + Liveonly bool + UnmanagedSpace bool + FreshlyBuildEnvironment bool + StageFolder string + ControllerType string + HolotreeSpace string + EnvironmentHash string + SemanticTag string + ForcedRobocorpHome string + When int64 + ProgressMark time.Time + Clock *stopwatch + randomIdentifier string ) func init() { diff --git a/common/version.go b/common/version.go index 979d4113..4a4c6371 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.7.1` + Version = `v13.8.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index aad46819..ef0746bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v13.8.0 (date: 7.3.2023) + +- new `--export` option to `rcc holotree prebuild` command, to enable direct + export to given hololib.zip filename of new, successfully build catalogs +- bugfix: catalog was exported before its content, which would make it so, that + catalog is present before its parts + ## v13.7.1 (date: 27.2.2023) - added missing `RCC_REMOTE_AUTHORIZATION` variable handling to rcc and passing diff --git a/htfs/commands.go b/htfs/commands.go index a3693d86..ac0bbcab 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -63,7 +63,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) - common.EnvironmentHash = BlueprintHash(holotreeBlueprint) + common.EnvironmentHash, common.FreshlyBuildEnvironment = BlueprintHash(holotreeBlueprint), false common.Progress(2, "Holotree blueprint is %q [%s].", common.EnvironmentHash, common.Platform()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) @@ -126,6 +126,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec common.Debug("Has blueprint environment: %v", exists) if force || !exists { + common.FreshlyBuildEnvironment = true remoteOrigin := common.RccRemoteOrigin() if len(remoteOrigin) > 0 { common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN.") diff --git a/htfs/library.go b/htfs/library.go index 004939d3..3a9167e2 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -191,10 +191,6 @@ func (it *hololib) Export(catalogs, known []string, archive string) (err error) for _, name := range catalogs { catalog := filepath.Join(common.HololibCatalogLocation(), name) - relative, err := filepath.Rel(common.HololibLocation(), catalog) - fail.On(err != nil, "Could not get relative location for catalog -> %v.", err) - err = zipper.Add(catalog, relative) - fail.On(err != nil, "Could not add catalog to zip -> %v.", err) fs, err := NewRoot(".") fail.On(err != nil, "Could not create root location -> %v.", err) @@ -202,6 +198,12 @@ func (it *hololib) Export(catalogs, known []string, archive string) (err error) fail.On(err != nil, "Could not load catalog from %s -> %v.", catalog, err) err = fs.Treetop(ZipRoot(it, fs, zipper)) fail.On(err != nil, "Could not zip catalog %s -> %v.", catalog, err) + + relative, err := filepath.Rel(common.HololibLocation(), catalog) + fail.On(err != nil, "Could not get relative location for catalog -> %v.", err) + err = zipper.Add(catalog, relative) + fail.On(err != nil, "Could not add catalog to zip -> %v.", err) + exported = true } fail.On(!exported, "None of given catalogs were available for export!") From 44391c230b4e268dae78cf912b8d989b6518be75 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Mar 2023 10:51:31 +0200 Subject: [PATCH 379/516] FEATURE: more strict hololib import (v13.9.0) - added initial support for verifying that holotree imported zip structure shape matches expected hololib catalog patterns (behind `--strict` flag, for now) --- cmd/holotreeImport.go | 19 +++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 ++++ operations/zipper.go | 63 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go index 2f8f350c..33a4b888 100644 --- a/cmd/holotreeImport.go +++ b/cmd/holotreeImport.go @@ -32,6 +32,20 @@ func temporaryDownload(at int, link string) (string, error) { return zipfile, nil } +func reportAllErrors(filename string, errors []error) error { + if errors == nil || len(errors) == 0 { + return nil + } + if len(errors) == 1 { + return errors[0] + } + common.Log("Errors from zip %q:", filename) + for at, err := range errors { + common.Log("- %d: %v", at+1, err) + } + return errors[0] +} + var holotreeImportCmd = &cobra.Command{ Use: "import hololib.zip+", Short: "Import one or more hololib.zip files into local hololib.", @@ -49,6 +63,11 @@ var holotreeImportCmd = &cobra.Command{ pretty.Guard(err == nil, 2, "Could not download %q, reason: %v", filename, err) defer os.Remove(filename) } + if common.StrictFlag { + errors := operations.VerifyZip(filename, operations.HololibZipShape) + err = reportAllErrors(filename, errors) + pretty.Guard(err == nil, 3, "Could not verify %q, first reason: %v", filename, err) + } err = operations.ProtectedImport(filename) pretty.Guard(err == nil, 1, "Could not import %q, reason: %v", filename, err) } diff --git a/common/version.go b/common/version.go index 4a4c6371..98783b41 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.8.0` + Version = `v13.9.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index ef0746bd..1fed8a8a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.9.0 (date: 8.3.2023) + +- added initial support for verifying that holotree imported zip structure shape + matches expected hololib catalog patterns (behind `--strict` flag, for now) + ## v13.8.0 (date: 7.3.2023) - new `--export` option to `rcc holotree prebuild` command, to enable direct diff --git a/operations/zipper.go b/operations/zipper.go index be7060a7..e57237c8 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" "github.com/robocorp/rcc/common" @@ -20,22 +21,40 @@ const ( slash = `/` ) +var ( + libraryPattern = regexp.MustCompile("(?i)^library/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{64}$") + catalogPattern = regexp.MustCompile("(?i)^catalog/[0-9a-f]{16}v[0-9a-f]{2}\\.(?:windows|darwin|linux)_(?:amd64|arm64)") +) + +type ( + Verifier func(file *zip.File) error + + WriteTarget struct { + Source *zip.File + Target string + } + + Command interface { + Execute() bool + } + + CommandChannel chan Command + CompletedChannel chan bool +) + func slashed(text string) string { return strings.Replace(text, backslash, slash, -1) } -type WriteTarget struct { - Source *zip.File - Target string -} - -type Command interface { - Execute() bool +func HololibZipShape(file *zip.File) error { + library := libraryPattern.MatchString(file.Name) + catalog := catalogPattern.MatchString(file.Name) + if !library && !catalog { + return fmt.Errorf("filename %q does not match Holotree catalog or library entry pattern.", file.Name) + } + return nil } -type CommandChannel chan Command -type CompletedChannel chan bool - func (it *WriteTarget) Execute() bool { err := it.execute() if err != nil { @@ -115,6 +134,17 @@ func loopExecutor(work CommandChannel, done CompletedChannel) { done <- true } +func (it *unzipper) VerifyShape(verifier Verifier) []error { + errors := []error{} + for _, entry := range it.reader.File { + err := verifier(entry) + if err != nil { + errors = append(errors, err) + } + } + return errors +} + func (it *unzipper) Explode(workers int, directory string) error { // This is PoC code, for parallel extraction common.Debug("Exploding:") @@ -348,6 +378,19 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { return FixDirectory(fullpath) } +func VerifyZip(zipfile string, verifier Verifier) []error { + common.TimelineBegin("zip verify %q [size: %s]", zipfile, pathlib.HumaneSize(zipfile)) + defer common.TimelineEnd() + + unzip, err := newUnzipper(zipfile, false) + if err != nil { + return []error{err} + } + defer unzip.Close() + + return unzip.VerifyShape(verifier) +} + func Unzip(directory, zipfile string, force, temporary, flatten bool) error { common.TimelineBegin("unzip %q [size: %s] to %q", zipfile, pathlib.HumaneSize(zipfile), directory) defer common.TimelineEnd() From 2d47c3cf1c28617f1822621d79f4ecd676f12eb2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 9 Mar 2023 14:13:33 +0200 Subject: [PATCH 380/516] BUGFIX: in verification, diagnostics, and documentation (v13.9.1) - bugfix: zip verification failed when Windows uses backslashes in paths - adding diagnostics around `ROBOCORP_HOME` location and robots - minor documentation updates in relation to `ROBOCORP_HOME` usage --- common/diagnostics.go | 22 ++++++++++ common/timeline.go | 2 +- common/version.go | 2 +- docs/README.md | 85 ++++++++++++++++++++------------------- docs/changelog.md | 6 +++ docs/recipes.md | 34 ++++++++++++++++ operations/diagnostics.go | 39 ++++++++++++++++++ operations/zipper.go | 4 +- robot/robot.go | 4 ++ 9 files changed, 153 insertions(+), 45 deletions(-) diff --git a/common/diagnostics.go b/common/diagnostics.go index 40e3a412..d26e9ea0 100644 --- a/common/diagnostics.go +++ b/common/diagnostics.go @@ -3,6 +3,9 @@ package common import ( "encoding/json" "fmt" + "path/filepath" + + "github.com/robocorp/rcc/fail" ) const ( @@ -74,3 +77,22 @@ func (it *DiagnosticStatus) AsJson() (string, error) { } return string(body), nil } + +func IsInsideRobocorpHome(location string) (_ bool, err error) { + defer fail.Around(&err) + + candidate, err := filepath.Abs(location) + fail.On(err != nil, "Failed to get absolute path to %q, reason: %v", location, err) + + rchome, err := filepath.Abs(RobocorpHome()) + fail.On(err != nil, "Failed to get absolute path to ROBOCORP_HOME, reason: %v", err) + + for len(rchome) <= len(candidate) { + if rchome == candidate { + return true, nil + } + candidate = filepath.Dir(candidate) + } + + return false, nil +} diff --git a/common/timeline.go b/common/timeline.go index 90b31429..1aa556aa 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -47,7 +47,7 @@ loop: if TimelineEnabled && death.Milliseconds() > 0 { history = append(history, &timevent{0, death, "Now."}) Log("---- rcc timeline ----") - Log(" # percent seconds event") + Log(" # percent seconds event [rcc %s]", Version) for at, event := range history { permille := event.when * 1000 / death percent := float64(permille) / 10.0 diff --git a/common/version.go b/common/version.go index 98783b41..f67a8e87 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.9.0` + Version = `v13.9.1` ) diff --git a/docs/README.md b/docs/README.md index 4fb2e2ea..c734325c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,48 +26,51 @@ ### 3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) #### 3.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) #### 3.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### 3.9 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree) -### 3.10 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree) -#### 3.10.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#one-time-setup) -#### 3.10.2 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#reverting-back-to-private-holotrees) -### 3.11 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) -### 3.12 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) -#### 3.12.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) -### 3.13 [Advanced network diagnostics](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#advanced-network-diagnostics) -#### 3.13.1 [Configuration](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#configuration) -### 3.14 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) -#### 3.14.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.14.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### 3.14.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.14.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) -#### 3.14.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.14.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.14.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.14.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.14.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.14.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.14.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) -### 3.15 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +### 3.9 [What is `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-robocorp-home) +#### 3.9.1 [Are there some rules for `ROBOCORP_HOME` variable?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#are-there-some-rules-for-robocorp-home-variable) +#### 3.9.2 [When you might actually need to setup `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#when-you-might-actually-need-to-setup-robocorp-home) +### 3.10 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree) +### 3.11 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree) +#### 3.11.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#one-time-setup) +#### 3.11.2 [Reverting back to private holotrees](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#reverting-back-to-private-holotrees) +### 3.12 [What can be controlled using environment variables?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-can-be-controlled-using-environment-variables) +### 3.13 [How to troubleshoot rcc setup and robots?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-troubleshoot-rcc-setup-and-robots) +#### 3.13.1 [Additional debugging options](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-debugging-options) +### 3.14 [Advanced network diagnostics](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#advanced-network-diagnostics) +#### 3.14.1 [Configuration](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#configuration) +### 3.15 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) #### 3.15.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) -#### 3.15.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) -#### 3.15.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) -#### 3.15.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) -#### 3.15.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) -### 3.16 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-cicd-pipeline-integration-with-rcc) -#### 3.16.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) -#### 3.16.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) -#### 3.16.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) -#### 3.16.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) -### 3.17 [How to setup custom templates?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-custom-templates) -#### 3.17.1 [Custom template configuration in `settings.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-in-settingsyaml-) -#### 3.17.2 [Custom template configuration file as `templates.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-file-as-templatesyaml-) -#### 3.17.3 [Custom template content in `templates.zip` file.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-content-in-templateszip-file) -#### 3.17.4 [Shared using `https:` protocol ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#shared-using-https-protocol-) -### 3.18 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) -### 3.19 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) -#### 3.19.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) -#### 3.19.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) -### 3.20 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) +#### 3.15.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) +#### 3.15.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.15.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.15.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.15.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.15.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.15.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.15.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.15.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.15.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +### 3.16 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) +#### 3.16.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) +#### 3.16.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) +#### 3.16.3 [What are `channels:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-channels) +#### 3.16.4 [What are `dependencies:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-dependencies) +#### 3.16.5 [What are `rccPostInstall:` scripts?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-rccpostinstall-scripts) +### 3.17 [How to do "old-school" CI/CD pipeline integration with rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-do-old-school-cicd-pipeline-integration-with-rcc) +#### 3.17.1 [The oldschoolci.sh script](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#the-oldschoolcish-script) +#### 3.17.2 [A setup.sh script for simulating variable injection.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#a-setupsh-script-for-simulating-variable-injection) +#### 3.17.3 [Simulating actual CI/CD step in local machine.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#simulating-actual-cicd-step-in-local-machine) +#### 3.17.4 [Additional notes](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#additional-notes) +### 3.18 [How to setup custom templates?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-custom-templates) +#### 3.18.1 [Custom template configuration in `settings.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-in-settingsyaml-) +#### 3.18.2 [Custom template configuration file as `templates.yaml`.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-configuration-file-as-templatesyaml-) +#### 3.18.3 [Custom template content in `templates.zip` file.](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#custom-template-content-in-templateszip-file) +#### 3.18.4 [Shared using `https:` protocol ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#shared-using-https-protocol-) +### 3.19 [Where can I find updates for rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#where-can-i-find-updates-for-rcc) +### 3.20 [What has changed on rcc?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-has-changed-on-rcc) +#### 3.20.1 [See changelog from git repo ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-changelog-from-git-repo-) +#### 3.20.2 [See that from your version of rcc directly ...](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#see-that-from-your-version-of-rcc-directly-) +### 3.21 [Can I see these tips as web page?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#can-i-see-these-tips-as-web-page) ## 4 [Profile Configuration](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#profile-configuration) ### 4.1 [What is profile?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-profile) #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) diff --git a/docs/changelog.md b/docs/changelog.md index 1fed8a8a..562536e6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v13.9.1 (date: 8.3.2023) + +- bugfix: zip verification failed when Windows uses backslashes in paths +- adding diagnostics around `ROBOCORP_HOME` location and robots +- minor documentation updates in relation to `ROBOCORP_HOME` usage + ## v13.9.0 (date: 8.3.2023) - added initial support for verifying that holotree imported zip structure shape diff --git a/docs/recipes.md b/docs/recipes.md index b9855543..dfa97023 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -360,6 +360,40 @@ rcc task shell --robot path/to/robot.yaml ``` +## What is `ROBOCORP_HOME`? + +It is environment variable level settings, that says where Robocorp tooling +can keep tooling specific files and configurations. It has default values, +and normal case is that defaults are fine. But if there is need to "relocate" +that somewhere else, then this environment variable does the trick. + +### Are there some rules for `ROBOCORP_HOME` variable? + +- go with defaults, unless you have very good reason to override it +- avoid using spaces or special characters in path that is `ROBOCORP_HOME`, + so stick to basic english letters and numbers +- never use your "home" directory as `ROBOCORP_HOME`, it will cause conflicts +- never share `ROBOCORP_HOME` between two users, it should be unique to each + different user account +- also keep it private and protected, other users should not have access + to that directory +- never use `ROBOCORP_HOME` as working directory for user, or any other + tools; this directory is only meant for Robocorp tooling to use, change, + and operate on +- never put `ROBOCORP_HOME` on network drive, since those tend to be slow, + and using those can cause real performance issues + + +### When you might actually need to setup `ROBOCORP_HOME`? + +- if your username contains spaces, or some special characters that can cause + tooling to break +- if path to your home directory is very long, it might cause long path issues, + and one way to go around is have `ROBOCORP_HOME` on shorter path +- if you need to have `ROBOCORP_HOME` on some different disk than default +- if your home directory is on HDD drive (or even network drive), but you + have fast SSD direve available, performance might be much better on SSD + ## What is shared holotree? Shared holotree is way to multiple users use same environment blueprint in diff --git a/operations/diagnostics.go b/operations/diagnostics.go index b634d785..14029125 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -119,6 +119,10 @@ func RunDiagnostics() *common.DiagnosticStatus { result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLibraryLocation())) } result.Checks = append(result.Checks, robocorpHomeCheck()) + check := workdirCheck() + if check != nil { + result.Checks = append(result.Checks, check) + } result.Checks = append(result.Checks, anyPathCheck("PYTHONPATH")) result.Checks = append(result.Checks, anyPathCheck("PLAYWRIGHT_BROWSERS_PATH")) result.Checks = append(result.Checks, anyPathCheck("NODE_OPTIONS")) @@ -288,6 +292,28 @@ func verifySharedDirectory(fullpath string) *common.DiagnosticCheck { } } +func workdirCheck() *common.DiagnosticCheck { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + workarea, err := os.Getwd() + if err != nil { + return nil + } + inside, err := common.IsInsideRobocorpHome(workarea) + if err != nil { + return nil + } + if inside { + return &common.DiagnosticCheck{ + Type: "RPA", + Category: common.CategoryPathCheck, + Status: statusWarning, + Message: fmt.Sprintf("Working directory %q is inside ROBOCORP_HOME (%s).", workarea, common.RobocorpHome()), + Link: supportGeneralUrl, + } + } + return nil +} + func robocorpHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { @@ -299,6 +325,19 @@ func robocorpHomeCheck() *common.DiagnosticCheck { Link: supportGeneralUrl, } } + userhome, err := os.UserHomeDir() + if err == nil { + inside, err := common.IsInsideRobocorpHome(userhome) + if err == nil && inside { + return &common.DiagnosticCheck{ + Type: "RPA", + Category: common.CategoryRobocorpHome, + Status: statusWarning, + Message: fmt.Sprintf("User home directory %q is inside ROBOCORP_HOME (%s).", userhome, common.RobocorpHome()), + Link: supportGeneralUrl, + } + } + } return &common.DiagnosticCheck{ Type: "RPA", Category: common.CategoryRobocorpHome, diff --git a/operations/zipper.go b/operations/zipper.go index e57237c8..f9a23b96 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -22,8 +22,8 @@ const ( ) var ( - libraryPattern = regexp.MustCompile("(?i)^library/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{64}$") - catalogPattern = regexp.MustCompile("(?i)^catalog/[0-9a-f]{16}v[0-9a-f]{2}\\.(?:windows|darwin|linux)_(?:amd64|arm64)") + libraryPattern = regexp.MustCompile("(?i)^library[/\\\\]{1,2}[0-9a-f]{2}[/\\\\]{1,2}[0-9a-f]{2}[/\\\\]{1,2}[0-9a-f]{2}[/\\\\]{1,2}[0-9a-f]{64}$") + catalogPattern = regexp.MustCompile("(?i)^catalog[/\\\\]{1,2}[0-9a-f]{16}v[0-9a-f]{2}\\.(?:windows|darwin|linux)_(?:amd64|arm64)") ) type ( diff --git a/robot/robot.go b/robot/robot.go index 62a83318..a9d27794 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -190,6 +190,10 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose := target.Diagnose("Robot") it.diagnoseTasks(diagnose) it.diagnoseVariousPaths(diagnose) + inside, err := common.IsInsideRobocorpHome(it.WorkingDirectory()) + if err == nil && inside { + diagnose.Warning(0, "", "Robot working directory %q is inside ROBOCORP_HOME (%s)", it.WorkingDirectory(), common.RobocorpHome()) + } if it.Artifacts == "" { diagnose.Fail(0, "", "In robot.yaml, 'artifactsDir:' is required!") } else { From 5615a485a067b3f10a7df0ac69ff4ba352075c6d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 9 Mar 2023 15:09:59 +0200 Subject: [PATCH 381/516] DOCS: toc generation improvement (v13.9.2) --- common/version.go | 2 +- docs/README.md | 6 +++--- docs/changelog.md | 6 +++++- scripts/toc.py | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index f67a8e87..4be2d38b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.9.1` + Version = `v13.9.2` ) diff --git a/docs/README.md b/docs/README.md index c734325c..58a07e69 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,9 +26,9 @@ ### 3.8 [How to control holotree environments?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-control-holotree-environments) #### 3.8.1 [How to get understanding on holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-get-understanding-on-holotree) #### 3.8.2 [How to activate holotree environment?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-activate-holotree-environment) -### 3.9 [What is `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-robocorp-home) -#### 3.9.1 [Are there some rules for `ROBOCORP_HOME` variable?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#are-there-some-rules-for-robocorp-home-variable) -#### 3.9.2 [When you might actually need to setup `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#when-you-might-actually-need-to-setup-robocorp-home) +### 3.9 [What is `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-robocorp_home) +#### 3.9.1 [Are there some rules for `ROBOCORP_HOME` variable?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#are-there-some-rules-for-robocorp_home-variable) +#### 3.9.2 [When you might actually need to setup `ROBOCORP_HOME`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#when-you-might-actually-need-to-setup-robocorp_home) ### 3.10 [What is shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-shared-holotree) ### 3.11 [How to setup rcc to use shared holotree?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#how-to-setup-rcc-to-use-shared-holotree) #### 3.11.1 [One time setup](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#one-time-setup) diff --git a/docs/changelog.md b/docs/changelog.md index 562536e6..e901bf4b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v13.9.1 (date: 8.3.2023) +## v13.9.2 (date: 9.3.2023) DOCUMENTATION + +- bugfix: updated toc.py to generate improved table of contents + +## v13.9.1 (date: 9.3.2023) - bugfix: zip verification failed when Windows uses backslashes in paths - adding diagnostics around `ROBOCORP_HOME` location and robots diff --git a/scripts/toc.py b/scripts/toc.py index 1e515a07..a0aaec3c 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -5,7 +5,7 @@ from os.path import basename DELETE_PATTERN = re.compile(r'[/:]+') -NONCHAR_PATTERN = re.compile(r'[^.a-z0-9-]+') +NONCHAR_PATTERN = re.compile(r'[^.a-z0-9_-]+') HEADING_PATTERN = re.compile(r'^\s*(#{1,3})\s+(.*?)\s*$') CODE_PATTERN = re.compile(r'^\s*[`]{3}') From 56e31dbde565396b8ea57a58b41036d42242d9bd Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 16 Mar 2023 13:19:35 +0200 Subject: [PATCH 382/516] DOCUMENTATION: vocabulary and maintenance (v13.10.0) - documentation: added holotree maintenance documentation - documentation: added vocabulary/glossary for rcc used terms - both above also as `rcc docs` subcommands --- cmd/man.go | 15 +++++ common/version.go | 2 +- docs/README.md | 72 ++++++++++++++++-------- docs/changelog.md | 6 ++ docs/maintenance.md | 79 +++++++++++++++++++++++++++ docs/vocabulary.md | 130 ++++++++++++++++++++++++++++++++++++++++++++ scripts/toc.py | 2 + 7 files changed, 283 insertions(+), 23 deletions(-) create mode 100644 docs/maintenance.md create mode 100644 docs/vocabulary.md diff --git a/cmd/man.go b/cmd/man.go index f05bba2b..3b88d16d 100644 --- a/cmd/man.go +++ b/cmd/man.go @@ -42,6 +42,13 @@ func init() { Run: makeShowDoc("LICENSE", "assets/man/LICENSE.txt"), }) + manCmd.AddCommand(&cobra.Command{ + Use: "maintenance", + Short: "Show holotree maintenance documentation.", + Long: "Show holotree maintenance documentation.", + Run: makeShowDoc("holotree maintenance documentation", "docs/maintenance.md"), + }) + manCmd.AddCommand(&cobra.Command{ Use: "profiles", Short: "Show configuration profiles documentation.", @@ -79,6 +86,14 @@ func init() { Run: makeShowDoc("tutorial", "assets/man/tutorial.txt"), } + manCmd.AddCommand(&cobra.Command{ + Use: "vocabulary", + Short: "Show vocabulary documentation", + Long: "Show vocabulary documentation", + Aliases: []string{"glossary", "lexicon"}, + Run: makeShowDoc("vocabulary documentation", "docs/vocabulary.md"), + }) + manCmd.AddCommand(tutorial) rootCmd.AddCommand(tutorial) } diff --git a/common/version.go b/common/version.go index 4be2d38b..89149f5f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.9.2` + Version = `v13.10.0` ) diff --git a/docs/README.md b/docs/README.md index 58a07e69..0175b1f2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -85,25 +85,53 @@ #### 4.7.1 ["I invited you to my home."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#i-invited-you-to-my-home) #### 4.7.2 ["Welcome to a hotel built out of ship containers."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-a-hotel-built-out-of-ship-containers) #### 4.7.3 ["Welcome to an actual Hotel."](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#welcome-to-an-actual-hotel) -## 5 [Troubleshooting guidelines and known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#troubleshooting-guidelines-and-known-solutions) -### 5.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) -### 5.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) -### 5.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) -### 5.4 [Network access related troubleshooting questions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#network-access-related-troubleshooting-questions) -### 5.5 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) -#### 5.5.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) -#### 5.5.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) -## 6 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) -### 6.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) -### 6.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) -### 6.3 [Version 9.x: between Jan 15, 2021 and Jun 10, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-9x-between-jan-15-2021-and-jun-10-2021) -### 6.4 [Version 8.x: between Jan 4, 2021 and Jan 18, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-8x-between-jan-4-2021-and-jan-18-2021) -### 6.5 [Version 7.x: between Dec 1, 2020 and Jan 4, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-7x-between-dec-1-2020-and-jan-4-2021) -### 6.6 [Version 6.x: between Nov 16, 2020 and Nov 30, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-6x-between-nov-16-2020-and-nov-30-2020) -### 6.7 [Version 5.x: between Nov 4, 2020 and Nov 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-5x-between-nov-4-2020-and-nov-16-2020) -### 6.8 [Version 4.x: between Oct 20, 2020 and Nov 2, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-4x-between-oct-20-2020-and-nov-2-2020) -### 6.9 [Version 3.x: between Oct 15, 2020 and Oct 19, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-3x-between-oct-15-2020-and-oct-19-2020) -### 6.10 [Version 2.x: between Sep 16, 2020 and Oct 14, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-2x-between-sep-16-2020-and-oct-14-2020) -### 6.11 [Version 1.x: between Sep 3, 2020 and Sep 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-1x-between-sep-3-2020-and-sep-16-2020) -### 6.12 [Version 0.x: between April 1, 2020 and Sep 8, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-0x-between-april-1-2020-and-sep-8-2020) -### 6.13 [Birth of "Codename: Conman"](https://github.com/robocorp/rcc/blob/master/docs/history.md#birth-of-codename-conman) \ No newline at end of file +## 5 [Holotree and library maintenance](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#holotree-and-library-maintenance) +### 5.1 [Why do maintenance?](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#why-do-maintenance) +### 5.2 [Shared holotree and maintenance](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#shared-holotree-and-maintenance) +### 5.3 [Maintenance vs. tools using holotrees](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#maintenance-vs-tools-using-holotrees) +### 5.4 [Deleting catalogs and spaces](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#deleting-catalogs-and-spaces) +### 5.5 [Keeping hololib consistent](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#keeping-hololib-consistent) +### 5.6 [Summary of maintenance related commands](https://github.com/robocorp/rcc/blob/master/docs/maintenance.md#summary-of-maintenance-related-commands) +## 6 [Troubleshooting guidelines and known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#troubleshooting-guidelines-and-known-solutions) +### 6.1 [Tools to help with troubleshooting issues](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#tools-to-help-with-troubleshooting-issues) +### 6.2 [How to troubleshoot issue you are having?](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#how-to-troubleshoot-issue-you-are-having) +### 6.3 [Reporting an issue](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#reporting-an-issue) +### 6.4 [Network access related troubleshooting questions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#network-access-related-troubleshooting-questions) +### 6.5 [Known solutions](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#known-solutions) +#### 6.5.1 [Access denied while building holotree environment (Windows)](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#access-denied-while-building-holotree-environment-windows) +#### 6.5.2 [Message "Serialized environment creation" repeats](https://github.com/robocorp/rcc/blob/master/docs/troubleshooting.md#message-serialized-environment-creation-repeats) +## 7 [Vocabulary](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#vocabulary) +### 7.1 [Blueprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#blueprint) +### 7.2 [Catalog](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#catalog) +### 7.3 [Controller](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#controller) +### 7.4 [Diagnostics](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#diagnostics) +### 7.5 [Dirty environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#dirty-environment) +### 7.6 [Environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#environment) +### 7.7 [Fingerprint](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#fingerprint) +### 7.8 [Holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#holotree) +### 7.9 [Hololib](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#hololib) +### 7.10 [Identity](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#identity) +### 7.11 [Platform](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#platform) +### 7.12 [Prebuild environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#prebuild-environment) +### 7.13 [Pristine environment](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#pristine-environment) +### 7.14 [Private holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#private-holotree) +### 7.15 [Profile](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#profile) +### 7.16 [Robot](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#robot) +### 7.17 [Shared holotree](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#shared-holotree) +### 7.18 [Space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#space) +### 7.19 [Unmanaged holotree space](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#unmanaged-holotree-space) +### 7.20 [User](https://github.com/robocorp/rcc/blob/master/docs/vocabulary.md#user) +## 8 [History of rcc](https://github.com/robocorp/rcc/blob/master/docs/history.md#history-of-rcc) +### 8.1 [Version 11.x: between Sep 6, 2021 and ...](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-11x-between-sep-6-2021-and-) +### 8.2 [Version 10.x: between Jun 15, 2021 and Sep 1, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-10x-between-jun-15-2021-and-sep-1-2021) +### 8.3 [Version 9.x: between Jan 15, 2021 and Jun 10, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-9x-between-jan-15-2021-and-jun-10-2021) +### 8.4 [Version 8.x: between Jan 4, 2021 and Jan 18, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-8x-between-jan-4-2021-and-jan-18-2021) +### 8.5 [Version 7.x: between Dec 1, 2020 and Jan 4, 2021](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-7x-between-dec-1-2020-and-jan-4-2021) +### 8.6 [Version 6.x: between Nov 16, 2020 and Nov 30, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-6x-between-nov-16-2020-and-nov-30-2020) +### 8.7 [Version 5.x: between Nov 4, 2020 and Nov 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-5x-between-nov-4-2020-and-nov-16-2020) +### 8.8 [Version 4.x: between Oct 20, 2020 and Nov 2, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-4x-between-oct-20-2020-and-nov-2-2020) +### 8.9 [Version 3.x: between Oct 15, 2020 and Oct 19, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-3x-between-oct-15-2020-and-oct-19-2020) +### 8.10 [Version 2.x: between Sep 16, 2020 and Oct 14, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-2x-between-sep-16-2020-and-oct-14-2020) +### 8.11 [Version 1.x: between Sep 3, 2020 and Sep 16, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-1x-between-sep-3-2020-and-sep-16-2020) +### 8.12 [Version 0.x: between April 1, 2020 and Sep 8, 2020](https://github.com/robocorp/rcc/blob/master/docs/history.md#version-0x-between-april-1-2020-and-sep-8-2020) +### 8.13 [Birth of "Codename: Conman"](https://github.com/robocorp/rcc/blob/master/docs/history.md#birth-of-codename-conman) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index e901bf4b..c0a8f5d3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v13.10.0 (date: 16.3.2023) + +- documentation: added holotree maintenance documentation +- documentation: added vocabulary/glossary for rcc used terms +- both above also as `rcc docs` subcommands + ## v13.9.2 (date: 9.3.2023) DOCUMENTATION - bugfix: updated toc.py to generate improved table of contents diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 00000000..91ae565d --- /dev/null +++ b/docs/maintenance.md @@ -0,0 +1,79 @@ +# Holotree and library maintenance + +This documentation section gives you brief ideas how to maintain your +holotree/hololib setup. + +## Why do maintenance? + +There are number of reasons for doing maintenance, some of which are: + +- running into problem with using holotree, and wanting to start over +- running out of disk space, and wanting to reduced used foot print +- remove old/unused spaces from holotree +- remove old/unused catalogs from hololib +- just to keep things running smoothly on future robot invocations + +## Shared holotree and maintenance + +When doing maintenance in shared holotree, you should be aware, that it might +affect other user accounts in same machine. So when you are doing system wide +maintenance in shared holotree, make sure that nothing is working on those +environments and catalogs that your maintenance targets to. + +## Maintenance vs. tools using holotrees + +When doing maintenance on any holotree, you should be aware, that if Robocorp +tooling (Worker, Assistant, Automation Studio, VS Code plugins, rcc, ...) is +also at same time using same holotree/hololib, your maintenance actions might +have negative effect on those tools. + +Some of those effects might be: + +- wiping environment under tool using it and causing automation, debugging, + editing, or development tooling to crash or produce unexpected results +- if catalog or space was removed, and it is needed later, then that must + be rebuild or downloaded, and that will slow down that use-case +- removing catalogs or hololib entries that will be needed by automations + or tooling, might cause slowness when needed next time, or if builds are + prevented it might even deny usage of those spaces + +## Deleting catalogs and spaces + +Before you delete anything, you should be aware of those things and what is +there. + +Catalogs can be listed using `rcc holotree catalogs` command, and +if you add `--identity` you can see what was their environment specification. + +Then command `rcc holotree list` is used to list those concrete spaces that +are consuming your disk space. + +Once you know what is there, and there are needs to remove catalogs, then +see `rcc holotree remove -h` for more about information on that. One good +option to use there is `--check 5` to also cleanup all released spare parts. + +And to free disk space consumed by concrete holotrees, see command +`rcc holotree delete -h`, which can be used to delete those spaces that +are not needed anymore. + +## Keeping hololib consistent + +And in cases, where there are holotree restoration problems, or hololib +issues, it is good to run consistency checks against that hololib. This +can be done using `rcc holotree check -h` command. And good option there +is to add `--retries 5` option, to get more "garbage collection cycles" +to maintain used disk space. + +Note that after running this command, and if there was something broken +inside hololib, then some of your catalogs have been removed, and in this +case it is good thing, since they were broken. And if they are needed in +future, those should be either build or imported. + +## Summary of maintenance related commands + +- `rcc holotree list -h` lists holotree spaces and their location +- `rcc holotree catalogs -h` list known catalogs, their blueprints, and stats +- `rcc configuration cleanup -h` for general cleanup procedures +- `rcc holotree delete -h` for deleting individual spaces +- `rcc holotree remove -h` for removing individual catalogs +- `rcc holotree check -h` for checking integrity of hololib diff --git a/docs/vocabulary.md b/docs/vocabulary.md new file mode 100644 index 00000000..c0991298 --- /dev/null +++ b/docs/vocabulary.md @@ -0,0 +1,130 @@ +# Vocabulary + +## Blueprint + +Is unique identity calculated from `conda.yaml` or in general from some +`environment.yaml` file after it is formatted in canonical, unified form. +Currently `rcc` uses "siphash" for that. This is form of a fingerprint. + +## Catalog + +Catalog is description how final created environment should look like, after +it is expanded and relocated into target location. Catalog is metadata, and +is used to verify that created environment matches original specification. + +## Controller + +This is tool or context that is currently running `rcc` command. + +## Diagnostics + +Network, configuration, or robot diagnostics, that are executed to give +status of one or some of those aspects. + +## Dirty environment + +An environment, holotree space specially, become dirty when after it is +restored in pristine state, something adds, deletes, or modifies files or +directories inside that specific space. This can happen by preRunScripts +modifying something when robot start, robot itself doing something that +changes actual environment, or when someone intentionally tries to modify +or install something into environment manually. + +When dirtyness is desired thing, like for developer purposes, use unmanaged +holotree spaces. But for normal automations and robots, it is good to start +from pristine, clean state. + +## Environment + +An environment here means either concrete holotree space, which contains +set of code and libraries (like python runtime environment) that are needed +for running specific robot or automation. Or it means that same environment +but as stashed away building blocks that are stored in hololib. + +## Fingerprint + +Fingerprint is normally a hash digest calculated from some content. Various +algorithms can be used for this, and some examples are Sha256 and siphash. + +## Holotree + +Is set of working areas, where concrete robots can run. Robots and processes +run inside one of these instances. These consume disk space. These are also +resetted into pristine state each time one of `rcc` run or environment related +subcommands are executes. + +## Hololib + +Is set of building blocks that are used to setup concreate holotree spaces. +Hololib contains both library and catalogs. Every unique content is stored +only once in library part. And catalogs refers to library fingerprints to +identify what parts they use. + +## Identity + +Identity is something that describes or identifies something uniquely. +For example `identity.yaml` is description that equals to `conda.yaml`. + +## Platform + +Platform refers to either Windows, MacOS, or Linux. And also either "amd64" +or "arm64" architectures of those. + +## Prebuild environment + +Prebuild environment is something that contains building blocks for full +holotree space in form of catalog + hololib parts. It is per operating system +and architecture, and can only be used in shared holotree context, where +parts are relocatable between different user accounts. + +During building concrete holotree space from prebuild environment, there is +no need for internet connection. If something inside robot run needs internet, +then that is not prebuild environment concern. + +## Pristine environment + +Environment that is restored to match exactly original, specified state. +When environments are used and content inside is changed, then those are +dirty/corrupted environments. They can be restored back to pristine state +using `rcc` commands. + +## Private holotree + +This is state, where all environments are created for single user, and cannot +be shared between users. These must be build and managed privately and using +them normally requires internet access. + +## Profile + +Profile is set of settings that describes network and Robocorp configurations +so that cloud and Control Room can be used in robot context. + +## Robot + +Robot is automation or process, that will be running inside one of concrete +holotree space. + +## Shared holotree + +This is state, where created environment can be relocated and different users +can use same shared catalogs to quickly replicate environments with identical +specifications, but provided for each user as separate space. + +## Space + +Concrete create environment where processes and robot actually run. Each +holotree space is identified by three things: user, controller, and space +identifier. Each different combination of those values receives their own +separate directory. These will each separately consume diskspace. + +## Unmanaged holotree space + +This is holotree space, that is created by `rcc` but it is not managed by +`rcc` after it gets created. It is up to user or using tool to manage and +maintain that environment. It can get dirty, can have traditional tooling +adding dependencies there, and it can deviate from specification. + +## User + +User account identity that is using `rcc`. Users wont share concrete holotrees +in shared holotree context. Each user will get their own separate space. diff --git a/scripts/toc.py b/scripts/toc.py index a0aaec3c..d842b11a 100755 --- a/scripts/toc.py +++ b/scripts/toc.py @@ -21,7 +21,9 @@ 'docs/recipes.md', 'docs/profile_configuration.md', 'docs/environment-caching.md', + 'docs/maintenance.md', 'docs/troubleshooting.md', + 'docs/vocabulary.md', 'docs/history.md', ) From d84c259cc28c911e67af69ebc1eaf33412525e52 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 20 Mar 2023 14:07:21 +0200 Subject: [PATCH 383/516] FIXES: diagnostics and documentation fixes (v13.10.1) - diagnostics: minor wording change (removing "toplevel" references) - documentation: some refinements and additions --- common/version.go | 2 +- docs/README.md | 2 ++ docs/changelog.md | 5 +++++ docs/features.md | 11 +++++++++-- docs/profile_configuration.md | 6 ++++++ docs/recipes.md | 6 +++--- docs/troubleshooting.md | 20 ++++++++++++-------- docs/usecases.md | 5 +++-- settings/data.go | 4 ++-- 9 files changed, 43 insertions(+), 18 deletions(-) diff --git a/common/version.go b/common/version.go index 89149f5f..cfe6ce79 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.10.0` + Version = `v13.10.1` ) diff --git a/docs/README.md b/docs/README.md index 0175b1f2..3b539fe3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,8 @@ #### 4.1.1 [When do you need profiles?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#when-do-you-need-profiles) #### 4.1.2 [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) ### 4.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) +#### 4.2.1 [Setup Utility -- user interface for this](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#setup-utility----user-interface-for-this) +#### 4.2.2 [Pure rcc workflow](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#pure-rcc-workflow) ### 4.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) ### 4.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) ### 4.5 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) diff --git a/docs/changelog.md b/docs/changelog.md index c0a8f5d3..e86a70d2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.10.1 (date: 20.3.2023) + +- diagnostics: minor wording change (removing "toplevel" references) +- documentation: some refinements and additions + ## v13.10.0 (date: 16.3.2023) - documentation: added holotree maintenance documentation diff --git a/docs/features.md b/docs/features.md index e8e297cf..dbb162f1 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,13 +1,14 @@ # Incomplete list of rcc features * supported operating systems are Windows, MacOS, and Linux +* supported sources for environment building are both conda and pypi * provide repeatable, isolated, and clean environments for robots to run * automatic environment creation based on declarative conda environment.yaml files * easily run software robots (automations) based on declarative robot.yaml files * test robots in isolated environments before uploading them to Control Room -* provide commands for Robocorp runtime and developer tools (Workforce Agent, - Assistant, Code, ...) +* provide commands for Robocorp runtime and developer tools (Worker, Assistant, + VS Code, Automation Studio ...) * provides commands to communicate with Robocorp Control Room from command line * enable caching dormant environments in efficiently and activating them locally when required without need to reinstall anything @@ -16,3 +17,9 @@ * support multiple configuration profiles for different network locations and conditions (remote, office, restricted networks, ...) * running assistants from command line +* support prebuild environments, where that environment was build elsewhere + and then just imported for local consumption +* allow "mass" prebuilding environments for delivery to those environments + where it is not desired to build those locally +* support unmanaged environments, where rcc only initially build environment + by the spec, but after that, does not do additional management of it diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md index 993f5f2a..c83ab2b9 100644 --- a/docs/profile_configuration.md +++ b/docs/profile_configuration.md @@ -24,6 +24,12 @@ can be active at any moment. ## Quick start guide +### Setup Utility -- user interface for this + +More behind [this link](https://robocorp.com/docs/control-room/setup-utility). + +### Pure rcc workflow + ```sh # interactively create "Office" profile rcc interactive configuration Office diff --git a/docs/recipes.md b/docs/recipes.md index dfa97023..8ffc30fe 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -163,8 +163,8 @@ rcc task script --interactive -- ipython ### What next? * Your python project is now converted to rcc and should be locally "runnable". -* Setup Assistant or Workforce Agent in your machine and create Assistant or - Robot in Robocorp Control Room, and try to run it from there. +* Setup Assistant or Worker in your machine and create Assistant or Robot + in Robocorp Control Room, and try to run it from there. * If your robot is "headless", has all dependencies, and should be runnable in Linux, then you can try to run it in container from Control Room. * If your project is python2 project, then consider converting it to python3. @@ -866,7 +866,7 @@ You then need to do these steps: URL that starts with https: Note: templates are needed only on development context, and they are not used -or needed in Assistant or Workforce Agent context. +or needed in Assistant or Worker context. ### Custom template configuration in `settings.yaml`. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6c7583e7..1aae6220 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -4,13 +4,16 @@ ## Tools to help with troubleshooting issues -- run command `rcc configuration diagnostics` and see if there are warnings - or errors in output (and same with `rcc configuration netdiagnostics`) +- run command `rcc configuration diagnostics` and see if there are warnings, + failures or errors in output (and same with `rcc configuration netdiagnostics`) - if failure is with specific robot, then try running command `rcc configuration diagnostics --robot path/to/robot.yaml` and see if - those robot diagnostics have something that identifies a problem + those robot diagnostics have something that identifies a problem (or to get + only robot diagnostics, you can also use `rcc robot diagnostics` command) - run command `rcc configuration speedtest` to see, if problem is actually performance related (like slow disk or network access) +- run command `rcc holotree check --retries 5` to verify (and fix possibly) + problems inside hololib storage - run rcc commands with `--debug` and `--timeline` flags, and see if anything there adds more information on why failure is happening @@ -39,7 +42,8 @@ - describe what error messages did you see - describe steps that are needed to be able to reproduce this issue - describe what have you already tried to resolve this issue -- describe what has changed since this was not present and everything worked ok +- describe what has changed since this issue was not present and when everything + worked ok - you should share your `conda.yaml` used with robot or environment - you should share your `robot.yaml` that defines your robot - you should share your code, or minimal sample code, that can reproduce @@ -49,12 +53,12 @@ - are you behind proxy, firewall, VPN, endpoint security solutions, or any combination of those? -- if you are, do you know, what brand are those products, and if they are - provided by third party service providers, who are those third parties? +- if you are, do you know, what brand are those products, and are they + provided by third party service providers, and who are those third parties? - are all those services configured to allow access to essential network places so that they don't cause interference on cloud access (change request or response headers, filter out URL parameters, change request or response - bodies, etc.)? + bodies, disallow DNS resolution, etc.)? - if those services require additional configuration in robot running machine, are those configurations in place in profiles used by rcc (service URLs, usernames and passwords, custom certificates, etc.)? @@ -71,7 +75,7 @@ If file is .dll or .exe file, then there is probably some process running, that has actually locked that file, and tooling cannot complete its operation while that other process is running. Other process might be virus scanner, some other -tool (Assistant, Workforce Agent, Automation Studio, VS Code, rcc) using same +tool (Assistant, Worker, Automation Studio, VS Code, rcc) using same environment, or even open Explorer view. To resolve this, close other applications, or wait them to finish before trying diff --git a/docs/usecases.md b/docs/usecases.md index d221f626..b97db886 100644 --- a/docs/usecases.md +++ b/docs/usecases.md @@ -1,6 +1,6 @@ # Incomplete list of rcc use cases -* run robots in Robocorp Workforce Agent locally or in cloud containers +* run robots in Robocorp Worker locally or in cloud containers * run robots in Robocorp Assistant * provide commands for Robocorp Code to develop robots locally and communicate to Robocorp Control Room @@ -12,7 +12,8 @@ conda in general) with isolated and easily installed manner (see list below for ideas what is available) * provide above things in computers, where internet access is restricted or - prohibited (using pre-made hololib.zip environments) + prohibited (using pre-made hololib.zip environments, or importing prebuild + environments build elsewhere) * pull and run community created robots without Control Room requirement ## What is available from conda-forge? diff --git a/settings/data.go b/settings/data.go index 6857d3e7..b2180c66 100644 --- a/settings/data.go +++ b/settings/data.go @@ -194,7 +194,7 @@ func (it *Settings) CriticalEnvironmentDiagnostics(target *common.DiagnosticStat correct = diagnoseUrl(it.Endpoints["downloads"], "endpoints/downloads", diagnose, correct) } if correct { - diagnose.Ok(0, "Toplevel settings are ok.") + diagnose.Ok(0, "Critical environment diagnostics are ok.") } } @@ -226,7 +226,7 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { correct = false } if correct { - diagnose.Ok(0, "Toplevel settings are ok.") + diagnose.Ok(0, "In general, 'settings.yaml' is ok.") } } From 684c43b138de9f5b34c7dffa2827dfd4c81ea600 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 5 Apr 2023 10:05:16 +0300 Subject: [PATCH 384/516] FEATURE: rcc file permissions (v13.11.0) - tighter permissions restrictions of rcc.yaml and rcccache.yaml using os.Chmod, so probably works on Mac and Linux, but Windows is uncertain --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/cache.go | 9 +++++++-- pathlib/functions.go | 4 ++++ xviper/wrapper.go | 2 ++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index cfe6ce79..d99bf95c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.10.1` + Version = `v13.11.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index e86a70d2..ac66cb25 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.11.0 (date: 5.4.2023) + +- tighter permissions restrictions of rcc.yaml and rcccache.yaml using + os.Chmod, so probably works on Mac and Linux, but Windows is uncertain + ## v13.10.1 (date: 20.3.2023) - diagnostics: minor wording change (removing "toplevel" references) diff --git a/operations/cache.go b/operations/cache.go index a40a8e8f..b150bdd0 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -73,11 +73,13 @@ func SummonCache() (*Cache, error) { } defer locker.Release() - source, err := os.Open(cacheLocation()) + cacheFile := cacheLocation() + source, err := os.Open(cacheFile) if err != nil { return result.Ready(), nil } defer source.Close() + defer pathlib.RestrictOwnerOnly(cacheFile) decoder := yaml.NewDecoder(source) err = decoder.Decode(&result) if err != nil { @@ -96,10 +98,13 @@ func (it *Cache) Save() error { } defer locker.Release() - sink, err := pathlib.Create(cacheLocation()) + cacheFile := cacheLocation() + sink, err := pathlib.Create(cacheFile) if err != nil { return err } + defer sink.Close() + defer pathlib.RestrictOwnerOnly(cacheFile) encoder := yaml.NewEncoder(sink) err = encoder.Encode(it) if err != nil { diff --git a/pathlib/functions.go b/pathlib/functions.go index 150fbfa6..f0cb42a8 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -22,6 +22,10 @@ func TempDir() string { return base } +func RestrictOwnerOnly(filename string) error { + return os.Chmod(filename, 0o600) +} + func Create(filename string) (*os.File, error) { _, err := EnsureParentDirectory(filename) if err != nil { diff --git a/xviper/wrapper.go b/xviper/wrapper.go index 5622d89a..db5ab968 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -52,6 +52,7 @@ func (it *config) Save() { common.Log("FATAL: could not write %v, reason %v; ignored.", it.Filename, err) return } + defer pathlib.RestrictOwnerOnly(it.Filename) when, err := pathlib.Modtime(it.Filename) if err == nil { it.Timestamp = when @@ -70,6 +71,7 @@ func (it *config) reload() { it.Viper = viper.New() it.Viper.SetConfigFile(it.Filename) + defer pathlib.RestrictOwnerOnly(it.Filename) err = it.Viper.ReadInConfig() var when time.Time if err == nil { From 1883bc0828fb30aa5f38b9fbee841ec096597a7b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 12 Apr 2023 08:34:22 +0300 Subject: [PATCH 385/516] FEATURE: micromamba upgrade (v13.12.0) - micromamba upgrade to v1.4.2 - test change: removed test that can fail because of probabilistic feature on some metric updates (which cause rcccache.yaml not to be written at all) --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 6 ++++++ robot_tests/fullrun.robot | 1 - 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index d99bf95c..4eee87b2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.11.0` + Version = `v13.12.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index d091bbd1..fd45458b 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_001_000 - MicromambaVersionNumber = "v1.1.0" + MicromambaVersionLimit = 1_004_002 + MicromambaVersionNumber = "v1.4.2" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index ac66cb25..c2f48733 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v13.12.0 (date: 12.4.2023) + +- micromamba upgrade to v1.4.2 +- test change: removed test that can fail because of probabilistic feature + on some metric updates (which cause rcccache.yaml not to be written at all) + ## v13.11.0 (date: 5.4.2023) - tighter permissions restrictions of rcc.yaml and rcccache.yaml using diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index aaffa377..3ba3b353 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -26,7 +26,6 @@ Goal: Telemetry tracking enabled by default. Step build/rcc configure identity --controller citests Must Have anonymous health tracking is: enabled Must Exist %{ROBOCORP_HOME}/rcc.yaml - Must Exist %{ROBOCORP_HOME}/rcccache.yaml Goal: Send telemetry data to cloud. Step build/rcc feedback metric --controller citests -t test -n rcc.test -v robot.fullrun From 0c70472c0ad48c9727e8a68dfd6559315ac466b0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 12 Apr 2023 11:59:29 +0300 Subject: [PATCH 386/516] FIX: additional path ignore (v13.12.1) - fix: added .poetry to list of ignored paths --- common/version.go | 2 +- conda/robocorp.go | 1 + docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 4eee87b2..457f9a94 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.12.0` + Version = `v13.12.1` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index fd45458b..3bb57a3c 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -30,6 +30,7 @@ var ( "pyenv", "venv", "pypoetry", + ".poetry", "virtualenv", } hashPattern = regexp.MustCompile("^[0-9a-f]{16}(?:\\.meta)?$") diff --git a/docs/changelog.md b/docs/changelog.md index c2f48733..e4f0e805 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.12.1 (date: 12.4.2023) + +- fix: added .poetry to list of ignored paths + ## v13.12.0 (date: 12.4.2023) - micromamba upgrade to v1.4.2 From da5d9aca02ccdfd54e5dc0cb71c3dc3c37ea5c1d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 13 Apr 2023 11:37:46 +0300 Subject: [PATCH 387/516] DOCS: additional documentation updated (v13.12.2) - updating documentation around `robot.yaml` and its functionality --- common/version.go | 2 +- docs/README.md | 19 ++++++++++--------- docs/changelog.md | 4 ++++ docs/recipes.md | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/common/version.go b/common/version.go index 457f9a94..3ebd4540 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.12.1` + Version = `v13.12.2` ) diff --git a/docs/README.md b/docs/README.md index 3b539fe3..25428d49 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,15 +41,16 @@ ### 3.15 [What is in `robot.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml) #### 3.15.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) #### 3.15.2 [What is this `robot.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-robotyaml-thing) -#### 3.15.3 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) -#### 3.15.4 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) -#### 3.15.5 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) -#### 3.15.6 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) -#### 3.15.7 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) -#### 3.15.8 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) -#### 3.15.9 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) -#### 3.15.10 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) -#### 3.15.11 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) +#### 3.15.3 [Why "the center of the universe"?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#why-the-center-of-the-universe) +#### 3.15.4 [What are `tasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-tasks) +#### 3.15.5 [What are `devTasks:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-devtasks) +#### 3.15.6 [What is `condaConfigFile:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-condaconfigfile) +#### 3.15.7 [What are `environmentConfigs:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs) +#### 3.15.8 [What are `preRunScripts:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-prerunscripts) +#### 3.15.9 [What is `artifactsDir:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-artifactsdir) +#### 3.15.10 [What are `ignoreFiles:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-ignorefiles) +#### 3.15.11 [What are `PATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-path) +#### 3.15.12 [What are `PYTHONPATH:`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-pythonpath) ### 3.16 [What is in `conda.yaml`?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml) #### 3.16.1 [Example](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#example) #### 3.16.2 [What is this `conda.yaml` thing?](https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-this-condayaml-thing) diff --git a/docs/changelog.md b/docs/changelog.md index e4f0e805..1203d4c9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v13.12.2 (date: 13.4.2023) + +- updating documentation around `robot.yaml` and its functionality + ## v13.12.1 (date: 12.4.2023) - fix: added .poetry to list of ignored paths diff --git a/docs/recipes.md b/docs/recipes.md index 8ffc30fe..bb1a5edb 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -563,14 +563,40 @@ PYTHONPATH: It is declarative description in [YAML format](https://en.wikipedia.org/wiki/YAML) of what robot is and what it can do. -It is also a pointer to "a robot center of universe" for directory it resides. +It is also a pointer to "a robot center of a universe" for directory it resides. So it is marker of "current working folder" when robot starts to execute and that will be indicated in `ROBOT_ROOT` environment variable. All declarations -inside `robot.yaml` should be relative to this location, so do not use -absolute paths here. +inside `robot.yaml` should be relative to and inside of this location, so do +not use absolute paths here, or relative references to any parent directory. + +It also marks root location that gets wrapped into `robot.zip` when either +wrapping locally or pushing to Control Room. Nothing above directory holding +`robot.yaml` gets wrapped into that zip file. Also note that `robot.yaml` is just a name of a file. Other names can be used -and then given to commands using `--robot othername.yaml` CLI option. +and then given to commands using `--robot othername.yaml` CLI option. But +in Robocorp tooling, this default name `robot.yaml` is used to have common +ground without additional configuration needs. + +### Why "the center of the universe"? + +Firstly, it is not "the center", it is just "a center of a universe" for +specific robot. So it only applies to that specific robot, when operations +are done around that one specific robot. Other robots have their own centers. + +And reason for thinking this way is, that it is "convention over configuration", +meaning that when we have this concept, there is much less configuration to do. +It gives following things automatically, without additional configuration: + +- what is "root" folder, when wrapping robot into deliverable package +- what is starting working directory when robot is executed (robot itself can + of course change its working directory freely while running) +- it gives solid starting point for relative paths inside robot, so that + PATH, PYTHONPATH, artifactsDir, and other relative references can be + converted absolute ones +- it allows robot location to be different for different users and on different + machines, and still have everything declared with known (but relative) + locations ### What are `tasks:`? From f81c82773306302144bb8b7d14eff5efbf899153 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 14 Apr 2023 12:10:25 +0300 Subject: [PATCH 388/516] IMPROVE: hololib corruption notes (v13.12.3) - improvement: more clear messaging on hololib corruption - fix: full cleanup will first remove catalogs and then hololib --- common/version.go | 2 +- conda/cleanup.go | 6 ++++-- docs/changelog.md | 5 +++++ htfs/functions.go | 2 +- pathlib/functions.go | 3 +++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index 3ebd4540..a4574e88 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.12.2` + Version = `v13.12.3` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 54492cb4..59ff2860 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -91,16 +91,18 @@ func spotlessCleanup(dryrun bool) error { } if dryrun { common.Log("- %v", BinMicromamba()) - common.Log("- %v", common.OldEventJournal()) common.Log("- %v", common.RobotCache()) - common.Log("- %v", common.HololibLocation()) + common.Log("- %v", common.OldEventJournal()) common.Log("- %v", common.JournalLocation()) + common.Log("- %v", common.HololibCatalogLocation()) + common.Log("- %v", common.HololibLocation()) return nil } safeRemove("executable", BinMicromamba()) safeRemove("cache", common.RobotCache()) safeRemove("old", common.OldEventJournal()) safeRemove("journals", common.JournalLocation()) + safeRemove("catalogs", common.HololibCatalogLocation()) return safeRemove("cache", common.HololibLocation()) } diff --git a/docs/changelog.md b/docs/changelog.md index 1203d4c9..feda5563 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v13.12.3 (date: 14.4.2023) + +- improvement: more clear messaging on hololib corruption +- fix: full cleanup will first remove catalogs and then hololib + ## v13.12.2 (date: 13.4.2023) - updating documentation around `robot.yaml` and its functionality diff --git a/htfs/functions.go b/htfs/functions.go index 949528e0..10b325e3 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -22,7 +22,7 @@ func JustFileExistCheck(library MutableLibrary, path, name, digest string) anywo location := library.ExactLocation(digest) if !pathlib.IsFile(location) { fullpath := filepath.Join(path, name) - panic(fmt.Errorf("Content for %q [%s] is missing!", fullpath, digest)) + panic(fmt.Errorf("Content for %q [%s] is missing; hololib is broken, requires check!", fullpath, digest)) } } } diff --git a/pathlib/functions.go b/pathlib/functions.go index f0cb42a8..d652ca90 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -54,6 +54,9 @@ func Exists(pathname string) bool { } func Abs(path string) (string, error) { + if filepath.IsAbs(path) { + return path, nil + } fullpath, err := filepath.Abs(path) if err != nil { return "", err From 27149295e739bcfaf47acb26bc11d957692fdf43 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 17 Apr 2023 12:39:54 +0300 Subject: [PATCH 389/516] MAJOR CHANGE: virtualenv support removed (v14.0.0) - major breaking change: this will remove some old, now unwanted functionality - this will be ongoing work for short while, making things unstable for now - removal of "virtual environment" support (pyvenv.cfg), and `VIRTUAL_ENV` variable is no longer available --- common/version.go | 2 +- conda/robocorp.go | 3 --- conda/workflows.go | 25 ------------------------- docs/changelog.md | 7 +++++++ robot_tests/fullrun.robot | 2 +- 5 files changed, 9 insertions(+), 30 deletions(-) diff --git a/common/version.go b/common/version.go index a4574e88..6f3f6a42 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v13.12.3` + Version = `v14.0.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 3bb57a3c..711a47a8 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -121,10 +121,8 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if !ok { python, ok = holotreePath.Which("python", FileExtensions) } - virtualenv := "" if ok { environment = append(environment, "PYTHON_EXE="+python) - virtualenv = location } environment = append(environment, "CONDA_DEFAULT_ENV=rcc", @@ -134,7 +132,6 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "PYTHONHOME=", "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", - "VIRTUAL_ENV="+virtualenv, "PYTHONNOUSERSITE=1", "PYTHONDONTWRITEBYTECODE=x", "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), diff --git a/conda/workflows.go b/conda/workflows.go index 86d8e323..e27f6298 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -19,13 +19,6 @@ import ( "github.com/robocorp/rcc/shell" ) -const ( - venvTemplate = `home = %s -include-system-site-packages = true -version = %s -` -) - func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } @@ -277,27 +270,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } - _, ok = HolotreePath(targetFolder).Which("python", FileExtensions) - if ok { - venvContent := fmt.Sprintf(venvTemplate, targetFolder, pythonVersionAt(targetFolder)) - venvFile := filepath.Join(targetFolder, "pyvenv.cfg") - err = pathlib.WriteFile(venvFile, []byte(venvContent), 0o644) - if err != nil { - return false, false - } - } - return true, false } -func pythonVersionAt(targetFolder string) string { - versionText, code, _ := LiveCapture(targetFolder, "python", "--version") - if code != 0 { - return "?.?.?" - } - return strings.Replace(versionText, "Python ", "", 1) -} - func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { var left, right *Environment var err error diff --git a/docs/changelog.md b/docs/changelog.md index feda5563..05b1205e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v14.0.0 (date: 17.4.2023) UNSTABLE + +- major breaking change: this will remove some old, now unwanted functionality +- this will be ongoing work for short while, making things unstable for now +- removal of "virtual environment" support (pyvenv.cfg), and `VIRTUAL_ENV` + variable is no longer available + ## v13.12.3 (date: 14.4.2023) - improvement: more clear messaging on hololib corruption diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 3ba3b353..d78b3c41 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v13. + Must Have v14. Goal: Show rcc license information. Step build/rcc man license --controller citests From df43439b637e39f525fbdb524893260be60d0c43 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 18 Apr 2023 09:09:44 +0300 Subject: [PATCH 390/516] BREAK: removal robot list command (v14.1.0) - major breaking change: removed `rcc robot list` command and history handling support (this was old Lab requested functionality) --- cmd/robotlist.go | 88 --------------------------------------- common/version.go | 2 +- docs/changelog.md | 5 +++ operations/initialize.go | 1 - operations/robots.go | 84 ------------------------------------- operations/zipper.go | 8 ---- robot_tests/fullrun.robot | 6 --- 7 files changed, 6 insertions(+), 188 deletions(-) delete mode 100644 cmd/robotlist.go delete mode 100644 operations/robots.go diff --git a/cmd/robotlist.go b/cmd/robotlist.go deleted file mode 100644 index 39019a70..00000000 --- a/cmd/robotlist.go +++ /dev/null @@ -1,88 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - "time" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - taskDirectory string -) - -const ( - timeFormat = `02.01.2006 15:04` -) - -func updateRobotDirectory(directory string) { - err := operations.UpdateRobot(directory) - if err != nil { - pretty.Exit(1, "Error: %v", err) - } -} - -func jsonRobots() { - robots, err := operations.ListRobots() - if err != nil { - pretty.Exit(1, "Error: %v", err) - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - err = encoder.Encode(robots) - if err != nil { - pretty.Exit(2, "Error: %v", err) - } -} - -func listRobots() { - if jsonFlag { - jsonRobots() - return - } - robots, err := operations.ListRobots() - if err != nil { - pretty.Exit(1, "Error: %v", err) - } - if len(robots) == 0 { - pretty.Exit(2, "Error: No robots found!") - } - common.Log("Updated at | Created at | Directory") - for _, robot := range robots { - updated := time.Unix(robot.Updated, 0) - created := time.Unix(robot.Created, 0) - status := "" - if robot.Deleted > 0 { - status = "" - } - common.Log("%v | %v | %v %v", updated.Format(timeFormat), created.Format(timeFormat), robot.Path, status) - } -} - -var robotlistCmd = &cobra.Command{ - Use: "list", - Short: "List or update tracked robot directories.", - Long: "List or update tracked robot directories.", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Robot list lasted").Report() - } - if len(taskDirectory) > 0 { - updateRobotDirectory(taskDirectory) - } else { - listRobots() - } - }, -} - -func init() { - robotCmd.AddCommand(robotlistCmd) - robotlistCmd.Flags().StringVarP(&taskDirectory, "add", "a", "", "The root directory to add as robot.") - robotlistCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") -} diff --git a/common/version.go b/common/version.go index 6f3f6a42..53c44288 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.0.0` + Version = `v14.1.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 05b1205e..ca00cab8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.1.0 (date: 18.4.2023) UNSTABLE + +- major breaking change: removed `rcc robot list` command and history handling + support (this was old Lab requested functionality) + ## v14.0.0 (date: 17.4.2023) UNSTABLE - major breaking change: this will remove some old, now unwanted functionality diff --git a/operations/initialize.go b/operations/initialize.go index 3e66a87c..93da2f83 100644 --- a/operations/initialize.go +++ b/operations/initialize.go @@ -243,6 +243,5 @@ func InitializeWorkarea(directory, name string, internal, force bool) error { if err != nil { return err } - UpdateRobot(fullpath) return unpack(content, fullpath) } diff --git a/operations/robots.go b/operations/robots.go deleted file mode 100644 index 38a51076..00000000 --- a/operations/robots.go +++ /dev/null @@ -1,84 +0,0 @@ -package operations - -import ( - "os" - "path/filepath" - "sort" - - "github.com/robocorp/rcc/common" -) - -func UpdateRobot(directory string) error { - fullpath, err := filepath.Abs(directory) - if err != nil { - return err - } - cache, err := SummonCache() - if err != nil { - return err - } - defer cache.Save() - robot, ok := cache.Robots[fullpath] - if !ok { - robot = &Folder{ - Path: fullpath, - Created: common.When, - Updated: common.When, - Deleted: 0, - } - cache.Robots[fullpath] = robot - } - stat, err := os.Stat(fullpath) - if err != nil || !stat.IsDir() { - robot.Deleted = common.When - } - robot.Updated = common.When - return nil -} - -func sorted(folders []*Folder) { - sort.SliceStable(folders, func(left, right int) bool { - if folders[left].Deleted != folders[right].Deleted { - return folders[left].Deleted < folders[right].Deleted - } - return folders[left].Updated > folders[right].Updated - }) -} - -func detectDeadRobots() bool { - cache, err := SummonCache() - if err != nil { - return false - } - changed := false - for _, robot := range cache.Robots { - stat, err := os.Stat(robot.Path) - if err != nil || !stat.IsDir() { - robot.Deleted = common.When - changed = true - continue - } - if robot.Deleted > 0 && stat.IsDir() { - robot.Deleted = 0 - changed = true - } - } - if changed { - cache.Save() - } - return changed -} - -func ListRobots() ([]*Folder, error) { - detectDeadRobots() - cache, err := SummonCache() - if err != nil { - return nil, err - } - result := make([]*Folder, 0, len(cache.Robots)) - for _, value := range cache.Robots { - result = append(result, value) - } - sorted(result) - return result, nil -} diff --git a/operations/zipper.go b/operations/zipper.go index f9a23b96..35521c9e 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -371,10 +371,6 @@ func CarrierUnzip(directory, carrier string, force, temporary bool) error { if temporary { return nil } - err = UpdateRobot(fullpath) - if err != nil { - return err - } return FixDirectory(fullpath) } @@ -419,10 +415,6 @@ func Unzip(directory, zipfile string, force, temporary, flatten bool) error { if temporary { return nil } - err = UpdateRobot(fullpath) - if err != nil { - return err - } return FixDirectory(fullpath) } diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index d78b3c41..c397e7ee 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -64,12 +64,6 @@ Goal: Initialize new standard robot into tmp/fluffy folder using force. Use STDERR Must Have OK. -Goal: There should now be fluffy in robot listing - Step build/rcc robot list --controller citests -j - Must Be Json Response - Must Have fluffy - Must Have "robot" - Goal: Fail to initialize new standard robot into tmp/fluffy without force. Step build/rcc robot init -i --controller citests -t extended -d tmp/fluffy 2 Use STDERR From fce1fd5a7c46f6b6a0a736f264d16dca188e6078 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 18 Apr 2023 16:09:52 +0300 Subject: [PATCH 391/516] FIX: cleanup offending directories (v14.2.0) - cleanup functionality for "Scripts" and "site-packages" that are in wrong place (due virtual environment bug fixed in v14.0.0) --- cmd/rcc/main.go | 3 +++ common/variables.go | 11 +++++++++++ common/version.go | 2 +- conda/cleanup.go | 17 ++++++++++++++++- docs/changelog.md | 5 +++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index fc8c51b7..9309bd95 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" ) @@ -95,6 +96,8 @@ func markTempForRecycling() { func main() { defer ExitProtection() + anywork.Backlog(conda.BugsCleanup) + if common.SharedHolotree { common.TimelineBegin("Start [shared mode]. (parent/pid: %d/%d)", os.Getppid(), os.Getpid()) } else { diff --git a/common/variables.go b/common/variables.go index 4a0b0d50..b05fc6ce 100644 --- a/common/variables.go +++ b/common/variables.go @@ -206,6 +206,17 @@ func HolotreeLock() string { return filepath.Join(HolotreeLocation(), "global.lck") } +func BadHololibSitePackagesLocation() string { + return filepath.Join(HololibLocation(), "site-packages") +} + +func BadHololibScriptsLocation() string { + if SharedHolotree { + return filepath.Join(HoloLocation(), "Scripts") + } + return filepath.Join(RobocorpHome(), "Scripts") +} + func UsesHolotree() bool { return len(HolotreeSpace) > 0 } diff --git a/common/version.go b/common/version.go index 53c44288..f7755fb9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.1.0` + Version = `v14.2.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 59ff2860..c9609d99 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -13,7 +13,7 @@ import ( func safeRemove(hint, pathling string) error { var err error if !pathlib.Exists(pathling) { - common.Debug("[%s] Missing %v, not need to remove.", hint, pathling) + common.Debug("[%s] Missing %v, no need to remove.", hint, pathling) return nil } if pathlib.IsDir(pathling) { @@ -41,6 +41,16 @@ func doCleanup(fullpath string, dryrun bool) error { return safeRemove("path", fullpath) } +func bugsCleanup(dryrun bool) { + if dryrun { + common.Log("- %v", common.BadHololibSitePackagesLocation()) + common.Log("- %v", common.BadHololibScriptsLocation()) + return + } + safeRemove("bugs", common.BadHololibSitePackagesLocation()) + safeRemove("bugs", common.BadHololibScriptsLocation()) +} + func alwaysCleanup(dryrun bool) { base := filepath.Join(common.RobocorpHome(), "base") live := filepath.Join(common.RobocorpHome(), "live") @@ -139,6 +149,10 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { return nil } +func BugsCleanup() { + bugsCleanup(false) +} + func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error { lockfile := common.RobocorpLock() completed := pathlib.LockWaitMessage(lockfile, "Serialized environment cleanup [robocorp lock]") @@ -151,6 +165,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error defer locker.Release() alwaysCleanup(dryrun) + bugsCleanup(dryrun) if downloads { return downloadCleanup(dryrun) diff --git a/docs/changelog.md b/docs/changelog.md index ca00cab8..7aef0710 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.2.0 (date: 18.4.2023) UNSTABLE + +- cleanup functionality for "Scripts" and "site-packages" that are in wrong + place (due virtual environment bug fixed in v14.0.0) + ## v14.1.0 (date: 18.4.2023) UNSTABLE - major breaking change: removed `rcc robot list` command and history handling From a4bfd497cf2f5e8ac4b3fa40e2d57be286f844c4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 Apr 2023 08:56:02 +0300 Subject: [PATCH 392/516] BREAK: removal robot fix command (v14.3.0) - major breaking change: removed `rcc robot fix` command (just command, internal functionality is still used by rcc) --- cmd/fix.go | 32 -------------------------------- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 cmd/fix.go diff --git a/cmd/fix.go b/cmd/fix.go deleted file mode 100644 index 8c511877..00000000 --- a/cmd/fix.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var fixCmd = &cobra.Command{ - Use: "fix", - Short: "Automatically fix known issues inside robots.", - Long: `Automatically fix known issues inside robots. Current fixes are: -- make files in PATH folder executable -- convert .sh newlines to unix form`, - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Fix run lasted").Report() - } - err := operations.FixRobot(robotFile) - if err != nil { - pretty.Exit(1, "Error: %v", err) - } - pretty.Ok() - }, -} - -func init() { - robotCmd.AddCommand(fixCmd) - fixCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file.") -} diff --git a/common/version.go b/common/version.go index f7755fb9..efd0d5cc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.2.0` + Version = `v14.3.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7aef0710..97028614 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.3.0 (date: 19.4.2023) UNSTABLE + +- major breaking change: removed `rcc robot fix` command (just command, + internal functionality is still used by rcc) + ## v14.2.0 (date: 18.4.2023) UNSTABLE - cleanup functionality for "Scripts" and "site-packages" that are in wrong From 4e2694cf4e0d2c0280b5521d85a80876ea953435 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 Apr 2023 11:39:45 +0300 Subject: [PATCH 393/516] BREAK: removal robot libs command (v14.4.0) - major breaking change: removed `rcc robot libs` command, since it is not used in tooling, and if needed, needs better design --- cmd/libs.go | 58 --------------------- cmd/sharedvariables.go | 1 + common/version.go | 2 +- conda/librarian.go | 115 ----------------------------------------- docs/changelog.md | 5 ++ 5 files changed, 7 insertions(+), 174 deletions(-) delete mode 100644 cmd/libs.go delete mode 100644 conda/librarian.go diff --git a/cmd/libs.go b/cmd/libs.go deleted file mode 100644 index 41cd65fd..00000000 --- a/cmd/libs.go +++ /dev/null @@ -1,58 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - channelFlag bool - pipFlag bool - dryFlag bool - - condaOption string - nameOption string - addMany []string - removeMany []string -) - -var libsCmd = &cobra.Command{ - Use: "libs", - Aliases: []string{"library", "libraries"}, - Short: "Manage library dependencies in an action oriented way.", - Long: "Manage library dependencies in an action oriented way.", - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Robot libs lasted").Report() - } - changes := &conda.Changes{ - Name: nameOption, - Pip: pipFlag, - Dryrun: dryFlag, - Channel: channelFlag, - Add: addMany, - Remove: removeMany, - } - output, err := conda.UpdateEnvironment(condaOption, changes) - if err != nil { - pretty.Exit(1, "Error: %v", err) - } - common.Stdout("%s\n", output) - pretty.Ok() - }, -} - -func init() { - robotCmd.AddCommand(libsCmd) - libsCmd.Flags().StringVarP(&nameOption, "name", "n", "", "Change the name of the configuration.") - libsCmd.Flags().StringVarP(&condaOption, "conda", "", "", "Full path to the conda environment configuration file (conda.yaml).") - libsCmd.MarkFlagRequired("conda") - libsCmd.Flags().StringArrayVarP(&addMany, "add", "a", []string{}, "Add new libraries as requirements.") - libsCmd.Flags().StringArrayVarP(&removeMany, "remove", "r", []string{}, "Remove existing libraries from requirements.") - libsCmd.Flags().BoolVarP(&channelFlag, "channel", "c", false, "Operate on channels (default is packages).") - libsCmd.Flags().BoolVarP(&pipFlag, "pip", "p", false, "Operate on pip packages (the default is to operate on conda packages).") - libsCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Do not save the end result, just show what would happen.") -} diff --git a/cmd/sharedvariables.go b/cmd/sharedvariables.go index 052a6d92..34cd0914 100644 --- a/cmd/sharedvariables.go +++ b/cmd/sharedvariables.go @@ -7,6 +7,7 @@ var ( forceFlag bool listFlag bool jsonFlag bool + dryFlag bool productionFlag bool verifiedFlag bool ) diff --git a/common/version.go b/common/version.go index efd0d5cc..3a7a1904 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.3.0` + Version = `v14.4.0` ) diff --git a/conda/librarian.go b/conda/librarian.go deleted file mode 100644 index a326dd88..00000000 --- a/conda/librarian.go +++ /dev/null @@ -1,115 +0,0 @@ -package conda - -type Changes struct { - Name string - Dryrun bool - Pip bool - Channel bool - Add []string - Remove []string -} - -func UpdateEnvironment(filename string, changes *Changes) (string, error) { - environment := SummonEnvironment(filename) - if changes.Channel { - updateChannels(environment, changes) - } else { - err := updatePackages(environment, changes) - if err != nil { - return "", err - } - } - if len(changes.Name) > 0 { - environment.Name = changes.Name - } - if changes.Dryrun { - return environment.AsYaml() - } - err := environment.SaveAs(filename) - if err != nil { - return "", err - } - return environment.AsYaml() -} - -func Index(search string, members []string) int { - for at, member := range members { - if member == search { - return at - } - } - return -1 -} - -func bitGuard(size int) uint64 { - return uint64(size & 0x0000_3fff_ffff_ffff) -} - -func updateChannels(environment *Environment, changes *Changes) { - predicted := uint64(bitGuard(len(changes.Add)) + bitGuard(len(environment.Channels))) - result := make([]string, 0, predicted) - for _, current := range environment.Channels { - if Index(current, changes.Remove) > -1 { - continue - } - result = append(result, current) - } - for _, here := range changes.Add { - if Index(here, result) > -1 { - continue - } - result = append(result, here) - } - environment.Channels = result -} - -func updatePackages(environment *Environment, changes *Changes) error { - adds := asDependencies(changes.Add) - removes := asDependencies(changes.Remove) - if changes.Pip { - result, err := composePackages(environment.Pip, adds, removes) - if err != nil { - return err - } - environment.Pip = result - } else { - result, err := composePackages(environment.Conda, adds, removes) - if err != nil { - return err - } - environment.Conda = result - } - return nil -} - -func composePackages(target []*Dependency, add []*Dependency, remove []*Dependency) ([]*Dependency, error) { - predicted := uint64(bitGuard(len(target)) + bitGuard(len(add))) - result := make([]*Dependency, 0, predicted) - for _, current := range target { - if current.Index(remove) > -1 { - continue - } - result = append(result, current) - } - for _, current := range add { - found := current.Index(result) - if found < 0 { - result = append(result, current) - continue - } - selected, err := current.ChooseSpecific(result[found]) - if err != nil { - return nil, err - } - result[found] = selected - } - return result, nil -} - -func asDependencies(labels []string) []*Dependency { - result := make([]*Dependency, 0, len(labels)) - for _, label := range labels { - result = append(result, AsDependency(label)) - } - return result -} diff --git a/docs/changelog.md b/docs/changelog.md index 97028614..db6c35fc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.4.0 (date: 19.4.2023) UNSTABLE + +- major breaking change: removed `rcc robot libs` command, since it is not + used in tooling, and if needed, needs better design + ## v14.3.0 (date: 19.4.2023) UNSTABLE - major breaking change: removed `rcc robot fix` command (just command, From 23509d9ed0328461f6c76249e9674578f74664ef Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 19 Apr 2023 16:07:12 +0300 Subject: [PATCH 394/516] Spring cleaning release (v14.4.1) - MAJOR BREAKING CHANGES: - under "spring cleaning" umbrella - virtual environment and `pyvenv.cfg` support removed after realization that holotree environments are not virtual environments, they are full environments and then some, they can also be called soft-containers - by trying to be virtual environment also caused bug in Windows, where `site-pacakges` and `Scripts` directories could be polluting all other environments as well, and that is why there is some cleanup in place now - removed old, unused functionality, specially commands `rcc robot fix`, `rcc robot libs`, and `rcc robot list` and their relating functionality --- README.md | 2 +- common/version.go | 2 +- docs/changelog.md | 13 +++++++++++++ docs/usecases.md | 2 ++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8fc8c82..51fd9883 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![RCC](/docs/title.png) -RCC is a set of tooling that allows you to create, manage, and distribute Python-based self-contained automation packages - or robots :robot: as we call them. +RCC is a set of tooling that allows you to create, manage, and distribute Python-based self-contained automation packages - or robots :robot: as we call them. And run them on soft-containers that have access to rest of your machine. Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) configuration file, `rcc` is a foundation that allows anyone to build and share automation with ease. diff --git a/common/version.go b/common/version.go index 3a7a1904..9518fb38 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.4.0` + Version = `v14.4.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index db6c35fc..919a1b67 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,18 @@ # rcc change log +## v14.4.1 (date: 19.4.2023) + +- MAJOR BREAKING CHANGES: + - under "spring cleaning" umbrella + - virtual environment and `pyvenv.cfg` support removed after realization + that holotree environments are not virtual environments, they are full + environments and then some, they can also be called soft-containers + - by trying to be virtual environment also caused bug in Windows, where + `site-pacakges` and `Scripts` directories could be polluting all other + environments as well, and that is why there is some cleanup in place now + - removed old, unused functionality, specially commands `rcc robot fix`, + `rcc robot libs`, and `rcc robot list` and their relating functionality + ## v14.4.0 (date: 19.4.2023) UNSTABLE - major breaking change: removed `rcc robot libs` command, since it is not diff --git a/docs/usecases.md b/docs/usecases.md index b97db886..35baa7c1 100644 --- a/docs/usecases.md +++ b/docs/usecases.md @@ -15,6 +15,8 @@ prohibited (using pre-made hololib.zip environments, or importing prebuild environments build elsewhere) * pull and run community created robots without Control Room requirement +* use rcc provided holotree environments as soft-containers (they are isolated + environments, but also have access to rest of your machine resources) ## What is available from conda-forge? From 784b0a08baee59d5de2357772e1808abcce64bf5 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 3 May 2023 14:22:51 +0300 Subject: [PATCH 395/516] Subprocess exit status visibility (v14.5.0) - subprocess exit codes are now visible, when subprocess fails (that is, when exit code in non-zero) - minor update on "custom templates" documentation --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ docs/recipes.md | 3 ++- shell/task.go | 6 +++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 9518fb38..7e0ec7b6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.4.1` + Version = `v14.5.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 919a1b67..35242998 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v14.5.0 (date: 3.5.2023) + +- subprocess exit codes are now visible, when subprocess fails (that is, when + exit code in non-zero) +- minor update on "custom templates" documentation + ## v14.4.1 (date: 19.4.2023) - MAJOR BREAKING CHANGES: diff --git a/docs/recipes.md b/docs/recipes.md index bb1a5edb..eb45d5a5 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -877,13 +877,14 @@ compromising variable content inside repository). ## How to setup custom templates? Custom templates allows making your own templates that can be used when -new robot is created. So if you have your standard way of doing things, +new robot is created. So if you have your own standard way of doing things, then custom template is good way to codify it. You then need to do these steps: - setup custom settings.yaml that point location where template configuration file is located (the templates.yaml file) +- if you are using profiles, then make above change in settings.yaml used there - create that custom templates.yaml configuration file that lists available templates, and where template bundle can be found (the templates.zip file) - and finally build that templates.zip to bundle together all those templates diff --git a/shell/task.go b/shell/task.go index 4199321d..883e8dc9 100644 --- a/shell/task.go +++ b/shell/task.go @@ -68,7 +68,11 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) common.Timeline("exec %q started", it.executable) common.Debug("PID #%d is %q.", command.Process.Pid, command) defer func() { - common.Debug("PID #%d finished: %v.", command.Process.Pid, command.ProcessState) + if command.ProcessState.ExitCode() != 0 { + common.Log("Process %d: %v, command: %s %s [%s/%d]", command.Process.Pid, command.ProcessState, it.executable, it.args, common.Version, os.Getpid()) + } else { + common.Debug("PID #%d finished: %v.", command.Process.Pid, command.ProcessState) + } }() err = command.Wait() exit, ok := err.(*exec.ExitError) From 7d86adeef9b65112061ac252361d7249fa8b5109 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 4 May 2023 07:13:05 +0300 Subject: [PATCH 396/516] Filter for quick diagnostics (v14.6.0) - adding `--quick` flag to diagnostics to filter out slow diagnostics - for now, "slow diagnostics" are mostly network related checks, some of subprocesses still get executed (like micromamba for example) --- cmd/diagnostics.go | 10 ++++++---- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 12 +++++++++--- operations/issues.go | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 9d3da2cd..c323f424 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -9,8 +9,9 @@ import ( ) var ( - fileOption string - robotOption string + fileOption string + robotOption string + quickFilterFlag bool ) var diagnosticsCmd = &cobra.Command{ @@ -22,7 +23,7 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Diagnostic run lasted").Report() } - _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag) + _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag, quickFilterFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -34,7 +35,8 @@ func init() { configureCmd.AddCommand(diagnosticsCmd) rootCmd.AddCommand(diagnosticsCmd) - diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") + diagnosticsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") + diagnosticsCmd.Flags().BoolVarP(&quickFilterFlag, "quick", "q", false, "Only run quick diagnostics.") diagnosticsCmd.Flags().StringVarP(&fileOption, "file", "f", "", "Save output into a file.") diagnosticsCmd.Flags().StringVarP(&robotOption, "robot", "r", "", "Full path to 'robot.yaml' configuration file. [optional]") diagnosticsCmd.Flags().BoolVarP(&productionFlag, "production", "p", false, "Checks for production level robots. [optional]") diff --git a/common/version.go b/common/version.go index 7e0ec7b6..d840e3f2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.5.0` + Version = `v14.6.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 35242998..2dfbc556 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v14.6.0 (date: 4.5.2023) + +- adding `--quick` flag to diagnostics to filter out slow diagnostics +- for now, "slow diagnostics" are mostly network related checks, some of + subprocesses still get executed (like micromamba for example) + ## v14.5.0 (date: 3.5.2023) - subprocess exit codes are now visible, when subprocess fails (that is, when diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 14029125..a5a980dc 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -57,7 +57,7 @@ func justText(source stringerr) string { return result } -func RunDiagnostics() *common.DiagnosticStatus { +func runDiagnostics(quick bool) *common.DiagnosticStatus { result := &common.DiagnosticStatus{ Details: make(map[string]string), Checks: []*common.DiagnosticCheck{}, @@ -132,6 +132,12 @@ func RunDiagnostics() *common.DiagnosticStatus { } result.Checks = append(result.Checks, lockpidsCheck()...) result.Checks = append(result.Checks, lockfilesCheck()...) + if quick { + return result + } + + // Move slow checks below this position + hostnames := settings.Global.Hostnames() dnsStopwatch := common.Stopwatch("DNS lookup time for %d hostnames was about", len(hostnames)) for _, host := range hostnames { @@ -529,13 +535,13 @@ func ProduceNetDiagnostics(body []byte, json bool) (*common.DiagnosticStatus, er return nil, nil } -func ProduceDiagnostics(filename, robotfile string, json, production bool) (*common.DiagnosticStatus, error) { +func ProduceDiagnostics(filename, robotfile string, json, production, quick bool) (*common.DiagnosticStatus, error) { file, err := fileIt(filename) if err != nil { return nil, err } defer file.Close() - result := RunDiagnostics() + result := runDiagnostics(quick) if len(robotfile) > 0 { addRobotDiagnostics(robotfile, result, production) } diff --git a/operations/issues.go b/operations/issues.go index bcf5f482..8bec8901 100644 --- a/operations/issues.go +++ b/operations/issues.go @@ -57,7 +57,7 @@ func createIssueZip(attachmentsFiles []string) (string, error) { func createDiagnosticsReport(robotfile string) (string, *common.DiagnosticStatus, error) { file := filepath.Join(common.RobocorpTemp(), "diagnostics.txt") - diagnostics, err := ProduceDiagnostics(file, robotfile, false, false) + diagnostics, err := ProduceDiagnostics(file, robotfile, false, false, false) if err != nil { return "", nil, err } From c0d371d59cd796b8155873813b45f89d0306662e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 15 May 2023 08:54:58 +0300 Subject: [PATCH 397/516] Layers on holotree creation (v14.7.0) - adding logical layers on holotree installation (visible on timeline) --- common/version.go | 2 +- conda/workflows.go | 105 ++++++++++++++++++++++++++++++++------------- docs/changelog.md | 4 ++ 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/common/version.go b/common/version.go index d840e3f2..547e2e00 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.6.0` + Version = `v14.7.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index e27f6298..1d642578 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -112,31 +112,9 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall } return success, nil } - -func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { - targetFolder := common.StageFolder - planfile := fmt.Sprintf("%s.plan", targetFolder) - planSink, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) - if err != nil { - return false, false - } - defer func() { - planSink.Close() - content, err := os.ReadFile(planfile) - if err == nil { - common.Log("%s", string(content)) - } - os.Remove(planfile) - }() - - planalyzer := NewPlanAnalyzer(true) - defer planalyzer.Close() - - planWriter := io.MultiWriter(planSink, planalyzer) - fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) - stopwatch := common.Stopwatch("installation plan") - fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) - fmt.Fprintf(planWriter, "%s\n", yaml) +func micromambaLayer(condaYaml, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, force bool) (bool, bool) { + common.TimelineBegin("Layer: micromamba") + defer common.TimelineEnd() common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) ttl := "57600" @@ -165,12 +143,20 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if observer.HasFailures(targetFolder) { return false, true } + return true, false +} + +func pipLayer(requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool, bool, string) { + common.TimelineBegin("Layer: pip") + defer common.TimelineEnd() + + pipUsed := false fmt.Fprintf(planWriter, "\n--- pip plan @%ss ---\n\n", stopwatch) python, pyok := FindPython(targetFolder) if !pyok { fmt.Fprintf(planWriter, "Note: no python in target folder: %s\n", targetFolder) } - pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() + pipCache, wheelCache := common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { common.Progress(7, "Skipping pip install phase -- no pip dependencies.") @@ -179,7 +165,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) common.Timeline("pip fail. no python found.") common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) - return false, false + return false, false, pipUsed, "" } common.Progress(7, "Running pip install phase. (pip v%s)", PipVersion(python)) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) @@ -188,18 +174,25 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip install phase ===") - code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") common.Fatal(fmt.Sprintf("Pip [%d/%x]", code, code), err) - return false, false + return false, false, pipUsed, "" } journal.CurrentBuildEvent().PipComplete() common.Timeline("pip done.") pipUsed = true } + return true, false, pipUsed, python +} + +func postInstallLayer(postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool) { + common.TimelineBegin("Layer: post install scripts") + defer common.TimelineEnd() + fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { common.Progress(8, "Post install scripts phase started.") @@ -224,6 +217,58 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } else { common.Progress(8, "Post install scripts phase skipped -- no scripts.") } + return true, false +} + +func holotreeLayers(condaYaml, requirementsText string, postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File, force bool) (bool, bool, bool, string) { + common.TimelineBegin("Holotree layers") + defer common.TimelineEnd() + + success, fatal := micromambaLayer(condaYaml, targetFolder, stopwatch, planWriter, force) + if !success { + return success, fatal, false, "" + } + success, fatal, pipUsed, python := pipLayer(requirementsText, targetFolder, stopwatch, planWriter, planSink) + if !success { + return success, fatal, pipUsed, python + } + success, fatal = postInstallLayer(postInstall, targetFolder, stopwatch, planWriter, planSink) + if !success { + return success, fatal, pipUsed, python + } + return true, false, pipUsed, python +} + +func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { + targetFolder := common.StageFolder + planfile := fmt.Sprintf("%s.plan", targetFolder) + planSink, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return false, false + } + defer func() { + planSink.Close() + content, err := os.ReadFile(planfile) + if err == nil { + common.Log("%s", string(content)) + } + os.Remove(planfile) + }() + + planalyzer := NewPlanAnalyzer(true) + defer planalyzer.Close() + + planWriter := io.MultiWriter(planSink, planalyzer) + fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) + stopwatch := common.Stopwatch("installation plan") + fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) + fmt.Fprintf(planWriter, "%s\n", yaml) + + success, fatal, pipUsed, python := holotreeLayers(condaYaml, requirementsText, postInstall, targetFolder, stopwatch, planWriter, planSink, force) + if !success { + return success, fatal + } + common.Progress(9, "Activate environment started phase.") common.Debug("=== activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) @@ -244,7 +289,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand := common.NewCommander(python, "-m", "pip", "check", "--no-color") pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") - code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) diff --git a/docs/changelog.md b/docs/changelog.md index 2dfbc556..5cf00213 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v14.7.0 (date: 15.5.2023) + +- adding logical layers on holotree installation (visible on timeline) + ## v14.6.0 (date: 4.5.2023) - adding `--quick` flag to diagnostics to filter out slow diagnostics From 704979f15f612e71e5afe1c9ed3026f211d5cbc4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 31 May 2023 14:02:42 +0300 Subject: [PATCH 398/516] Layered fingerpring support (v14.8.0) - support for separating layers and calculating their fingerprints - showing fingerprints on build output and in timeline (still only visualization) - added controlling flag `--layered` to enable layer handling - added recording of layers if above flag is given --- cmd/holotreeBlueprints.go | 2 +- cmd/holotreeExport.go | 2 +- cmd/holotreeHash.go | 2 +- cmd/holotreePull.go | 2 +- cmd/holotreeVariables.go | 2 +- cmd/root.go | 1 + common/algorithms.go | 16 ++++++++ common/timeline.go | 2 +- common/variables.go | 1 + common/version.go | 2 +- conda/condayaml.go | 42 ++++++++++++++++--- conda/condayaml_test.go | 27 +++++++++++++ conda/testdata/layers.yaml | 9 +++++ conda/workflows.go | 83 +++++++++++++++++++++++--------------- docs/changelog.md | 7 ++++ htfs/commands.go | 6 +-- htfs/directory.go | 2 +- htfs/library.go | 43 +++++++++----------- htfs/unmanaged.go | 10 +++-- htfs/virtual.go | 12 ++++-- htfs/ziplibrary.go | 8 ++-- 21 files changed, 197 insertions(+), 84 deletions(-) create mode 100644 conda/testdata/layers.yaml diff --git a/cmd/holotreeBlueprints.go b/cmd/holotreeBlueprints.go index c2a47da4..48fd7551 100644 --- a/cmd/holotreeBlueprints.go +++ b/cmd/holotreeBlueprints.go @@ -21,7 +21,7 @@ func holotreeExpandBlueprint(userFiles []string, packfile string) map[string]int tree, err := htfs.New() pretty.Guard(err == nil, 6, "%s", err) - result["hash"] = htfs.BlueprintHash(holotreeBlueprint) + result["hash"] = common.BlueprintHash(holotreeBlueprint) result["exist"] = tree.HasBlueprint(holotreeBlueprint) return result diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index bdfb99f6..a26d074c 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -72,7 +72,7 @@ var holotreeExportCmd = &cobra.Command{ if len(exportRobot) > 0 { _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, exportRobot) pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) - hash := htfs.BlueprintHash(holotreeBlueprint) + hash := common.BlueprintHash(holotreeBlueprint) args = append(args, htfs.CatalogName(hash)) } if len(args) == 0 { diff --git a/cmd/holotreeHash.go b/cmd/holotreeHash.go index 0e4ce8d7..a52fc5d8 100644 --- a/cmd/holotreeHash.go +++ b/cmd/holotreeHash.go @@ -19,7 +19,7 @@ var holotreeHashCmd = &cobra.Command{ } _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(args, "") pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) - hash := htfs.BlueprintHash(holotreeBlueprint) + hash := common.BlueprintHash(holotreeBlueprint) common.Log("Blueprint hash for %v is %v.", args, hash) if common.Silent { common.Stdout("%s\n", hash) diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index 2336ab92..b668e00e 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -24,7 +24,7 @@ var holotreePullCmd = &cobra.Command{ } _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, pullRobot) pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) - hash := htfs.BlueprintHash(holotreeBlueprint) + hash := common.BlueprintHash(holotreeBlueprint) tree, err := htfs.New() pretty.Guard(err == nil, 2, "%s", err) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index e264f2d8..d01748ee 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -71,7 +71,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp config, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(userFiles, packfile) pretty.Guard(err == nil, 5, "%s", err) - condafile := filepath.Join(common.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) + condafile := filepath.Join(common.RobocorpTemp(), common.BlueprintHash(holotreeBlueprint)) err = pathlib.WriteFile(condafile, holotreeBlueprint, 0o644) pretty.Guard(err == nil, 6, "%s", err) diff --git a/cmd/root.go b/cmd/root.go index d26c86ca..5f714395 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -119,6 +119,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.LayeredHolotree, "layered", "", false, "use layered holotree spaces, experimental, DO NOT USE (unless you know what you are doing)") } func initConfig() { diff --git a/common/algorithms.go b/common/algorithms.go index d383574d..46304ca2 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -64,3 +64,19 @@ func OneOutOf(limit uint8) bool { } return true } + +func BlueprintHash(blueprint []byte) string { + return Textual(Sipit(blueprint), 0) +} + +func Sipit(key []byte) uint64 { + return Siphash(9007199254740993, 2147483647, key) +} + +func Textual(key uint64, size int) string { + text := fmt.Sprintf("%016x", key) + if size > 0 { + return text[:size] + } + return text +} diff --git a/common/timeline.go b/common/timeline.go index 1aa556aa..a836834b 100644 --- a/common/timeline.go +++ b/common/timeline.go @@ -52,7 +52,7 @@ loop: permille := event.when * 1000 / death percent := float64(permille) / 10.0 indent := strings.Repeat("| ", event.level) - Log("%2d: %5.1f%% %7s %s%s", at+1, percent, event.when, indent, event.what) + Log("%3d: %5.1f%% %7s %s%s", at+1, percent, event.when, indent, event.what) } Log("---- rcc timeline ----") } diff --git a/common/variables.go b/common/variables.go index b05fc6ce..253ea0b8 100644 --- a/common/variables.go +++ b/common/variables.go @@ -32,6 +32,7 @@ var ( Liveonly bool UnmanagedSpace bool FreshlyBuildEnvironment bool + LayeredHolotree bool StageFolder string ControllerType string HolotreeSpace string diff --git a/common/version.go b/common/version.go index 547e2e00..f8bd32dc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.7.0` + Version = `v14.8.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index d046eb78..c2a0ee5d 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -416,11 +416,23 @@ func (it *Environment) PipMap() map[interface{}]interface{} { func (it *Environment) AsPureConda() *Environment { return &Environment{ - Name: it.Name, - Prefix: it.Prefix, - Channels: it.Channels, - Conda: it.Conda, - Pip: []*Dependency{}, + Name: it.Name, + Prefix: it.Prefix, + Channels: it.Channels, + Conda: it.Conda, + Pip: []*Dependency{}, + PostInstall: []string{}, + } +} + +func (it *Environment) WithoutPostInstall() *Environment { + return &Environment{ + Name: it.Name, + Prefix: it.Prefix, + Channels: it.Channels, + Conda: it.Conda, + Pip: it.Pip, + PostInstall: []string{}, } } @@ -465,6 +477,26 @@ func (it *Environment) AsRequirementsText() string { return strings.Join(lines, Newline) } +func (it *Environment) AsLayers() [3]string { + conda, _ := it.AsPureConda().AsYaml() + pip, _ := it.WithoutPostInstall().AsYaml() + full, _ := it.AsYaml() + return [3]string{ + strings.TrimSpace(conda), + strings.TrimSpace(pip), + strings.TrimSpace(full), + } +} + +func (it *Environment) FingerprintLayers() [3]string { + layers := it.AsLayers() + return [3]string{ + common.BlueprintHash([]byte(layers[0])), + common.BlueprintHash([]byte(layers[1])), + common.BlueprintHash([]byte(layers[2])), + } +} + func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production bool) { diagnose := target.Diagnose("Conda") notice := diagnose.Warning diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index a5636847..aad11a50 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -3,6 +3,7 @@ package conda_test import ( "testing" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/hamlet" ) @@ -117,3 +118,29 @@ func TestCanCreateEmptyEnvironment(t *testing.T) { sut := conda.SummonEnvironment("tmp/missing.yaml") wont_be.Nil(sut) } + +func TestCanGetLayersFromCondaYaml(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + sut, err := conda.ReadCondaYaml("testdata/layers.yaml") + must_be.Nil(err) + wont_be.Nil(sut) + + layers := sut.AsLayers() + wont_be.Nil(layers) + wont_be.Equal(len(layers[0]), 0) + must_be.True(len(layers[0]) < len(layers[1])) + must_be.True(len(layers[1]) < len(layers[2])) + wont_be.Equal(layers[0], layers[1]) + wont_be.Equal(layers[0], layers[2]) + wont_be.Equal(layers[1], layers[2]) + + must_be.Equal("0d8cc85130420984", common.BlueprintHash([]byte(layers[0]))) + must_be.Equal("5be3e197c8c2c67d", common.BlueprintHash([]byte(layers[1]))) + must_be.Equal("d310697aca0840a1", common.BlueprintHash([]byte(layers[2]))) + + fingerprints := sut.FingerprintLayers() + must_be.Equal("0d8cc85130420984", fingerprints[0]) + must_be.Equal("5be3e197c8c2c67d", fingerprints[1]) + must_be.Equal("d310697aca0840a1", fingerprints[2]) +} diff --git a/conda/testdata/layers.yaml b/conda/testdata/layers.yaml new file mode 100644 index 00000000..cbee87c6 --- /dev/null +++ b/conda/testdata/layers.yaml @@ -0,0 +1,9 @@ +channels: +- conda-forge +dependencies: +- python=3.9.13 +- pip=22.1.2 +- pip: + - rpaframework==22.5.3 +rccPostInstall: +- python3 -m pip --version diff --git a/conda/workflows.go b/conda/workflows.go index 1d642578..04ffc34d 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -19,6 +19,12 @@ import ( "github.com/robocorp/rcc/shell" ) +type ( + Recorder interface { + Record([]byte) error + } +) + func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } @@ -80,7 +86,7 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { return false } -func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, error) { +func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, finalEnv *Environment, recorder Recorder) (bool, error) { if !MustMicromamba() { return false, fmt.Errorf("Could not get micromamba installed.") } @@ -93,7 +99,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall } common.Debug("=== first try phase ===") common.Timeline("first try.") - success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) + success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv, recorder) if !success && !force && !fatal { journal.CurrentBuildEvent().Rebuild() cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) @@ -105,15 +111,15 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall if err != nil { return false, err } - success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, postInstall) + success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, finalEnv, recorder) } if success { journal.CurrentBuildEvent().Successful() } return success, nil } -func micromambaLayer(condaYaml, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, force bool) (bool, bool) { - common.TimelineBegin("Layer: micromamba") +func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, force bool) (bool, bool) { + common.TimelineBegin("Layer: micromamba [%s]", fingerprint) defer common.TimelineEnd() common.Debug("Setting up new conda environment using %v to folder %v", condaYaml, targetFolder) @@ -121,7 +127,7 @@ func micromambaLayer(condaYaml, targetFolder string, stopwatch fmt.Stringer, pla if force { ttl = "0" } - common.Progress(6, "Running micromamba phase. (micromamba v%s)", MicromambaVersion()) + common.Progress(6, "Running micromamba phase. (micromamba v%s) [layer: %s]", MicromambaVersion(), fingerprint) mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -146,8 +152,8 @@ func micromambaLayer(condaYaml, targetFolder string, stopwatch fmt.Stringer, pla return true, false } -func pipLayer(requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool, bool, string) { - common.TimelineBegin("Layer: pip") +func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool, bool, string) { + common.TimelineBegin("Layer: pip [%s]", fingerprint) defer common.TimelineEnd() pipUsed := false @@ -167,7 +173,7 @@ func pipLayer(requirementsText, targetFolder string, stopwatch fmt.Stringer, pla common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) return false, false, pipUsed, "" } - common.Progress(7, "Running pip install phase. (pip v%s)", PipVersion(python)) + common.Progress(7, "Running pip install phase. (pip v%s) [layer: %s]", PipVersion(python), fingerprint) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander(python, "-m", "pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -189,13 +195,13 @@ func pipLayer(requirementsText, targetFolder string, stopwatch fmt.Stringer, pla return true, false, pipUsed, python } -func postInstallLayer(postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool) { - common.TimelineBegin("Layer: post install scripts") +func postInstallLayer(fingerprint string, postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool) { + common.TimelineBegin("Layer: post install scripts [%s]", fingerprint) defer common.TimelineEnd() fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - common.Progress(8, "Post install scripts phase started.") + common.Progress(8, "Post install scripts phase started. [layer: %s]", fingerprint) common.Debug("=== post install phase ===") for _, script := range postInstall { scriptCommand, err := shell.Split(script) @@ -220,26 +226,38 @@ func postInstallLayer(postInstall []string, targetFolder string, stopwatch fmt.S return true, false } -func holotreeLayers(condaYaml, requirementsText string, postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File, force bool) (bool, bool, bool, string) { +func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File, force bool, recorder Recorder) (bool, bool, bool, string) { common.TimelineBegin("Holotree layers") defer common.TimelineEnd() - success, fatal := micromambaLayer(condaYaml, targetFolder, stopwatch, planWriter, force) + pipNeeded := len(requirementsText) > 0 + postInstall := len(finalEnv.PostInstall) > 0 + + layers := finalEnv.AsLayers() + fingerprints := finalEnv.FingerprintLayers() + + success, fatal := micromambaLayer(fingerprints[0], condaYaml, targetFolder, stopwatch, planWriter, force) if !success { return success, fatal, false, "" } - success, fatal, pipUsed, python := pipLayer(requirementsText, targetFolder, stopwatch, planWriter, planSink) + if common.LayeredHolotree && (pipNeeded || postInstall) { + recorder.Record([]byte(layers[0])) + } + success, fatal, pipUsed, python := pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter, planSink) if !success { return success, fatal, pipUsed, python } - success, fatal = postInstallLayer(postInstall, targetFolder, stopwatch, planWriter, planSink) + if common.LayeredHolotree && pipUsed && postInstall { + recorder.Record([]byte(layers[1])) + } + success, fatal = postInstallLayer(fingerprints[2], finalEnv.PostInstall, targetFolder, stopwatch, planWriter, planSink) if !success { return success, fatal, pipUsed, python } return true, false, pipUsed, python } -func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { +func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, finalEnv *Environment, recorder Recorder) (bool, bool) { targetFolder := common.StageFolder planfile := fmt.Sprintf("%s.plan", targetFolder) planSink, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) @@ -264,7 +282,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) fmt.Fprintf(planWriter, "%s\n", yaml) - success, fatal, pipUsed, python := holotreeLayers(condaYaml, requirementsText, postInstall, targetFolder, stopwatch, planWriter, planSink, force) + success, fatal, pipUsed, python := holotreeLayers(condaYaml, requirementsText, finalEnv, targetFolder, stopwatch, planWriter, planSink, force, recorder) if !success { return success, fatal } @@ -309,33 +327,32 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh os.Rename(planfile, finalplan) common.Debug("=== finalize phase ===") - markerFile := filepath.Join(targetFolder, "identity.yaml") - err = pathlib.WriteFile(markerFile, []byte(yaml), 0o644) - if err != nil { - return false, false - } - return true, false } -func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { - var left, right *Environment - var err error - +func mergedEnvironment(filenames ...string) (right *Environment, err error) { for _, filename := range filenames { - left = right + left := right right, err = ReadCondaYaml(filename) if err != nil { - return "", "", nil, err + return nil, err } if left == nil { continue } right, err = left.Merge(right) if err != nil { - return "", "", nil, err + return nil, err } } + return right, nil +} + +func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { + right, err := mergedEnvironment(filenames...) + if err != nil { + return "", "", nil, err + } yaml, err := right.AsYaml() if err != nil { return "", "", nil, err @@ -354,7 +371,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. return hash, yaml, right, err } -func LegacyEnvironment(force bool, configurations ...string) error { +func LegacyEnvironment(recorder Recorder, force bool, configurations ...string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() @@ -379,7 +396,7 @@ func LegacyEnvironment(force bool, configurations ...string) error { defer os.Remove(condaYaml) defer os.Remove(requirementsText) - success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) + success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv, recorder) if err != nil { return err } diff --git a/docs/changelog.md b/docs/changelog.md index 5cf00213..e3e7aa07 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v14.8.0 (date: 31.5.2023) UNSTABLE + +- support for separating layers and calculating their fingerprints +- showing fingerprints on build output and in timeline (still only visualization) +- added controlling flag `--layered` to enable layer handling +- added recording of layers if above flag is given + ## v14.7.0 (date: 15.5.2023) - adding logical layers on holotree installation (visible on timeline) diff --git a/htfs/commands.go b/htfs/commands.go index ac0bbcab..82cb8304 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -63,7 +63,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) - common.EnvironmentHash, common.FreshlyBuildEnvironment = BlueprintHash(holotreeBlueprint), false + common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false common.Progress(2, "Holotree blueprint is %q [%s].", common.EnvironmentHash, common.Platform()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) @@ -130,7 +130,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec remoteOrigin := common.RccRemoteOrigin() if len(remoteOrigin) > 0 { common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN.") - hash := BlueprintHash(blueprint) + hash := common.BlueprintHash(blueprint) catalog := CatalogName(hash) err = puller(remoteOrigin, catalog, false) if err != nil { @@ -155,7 +155,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = os.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) - err = conda.LegacyEnvironment(force, identityfile) + err = conda.LegacyEnvironment(tree, force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) scorecard.Midpoint() diff --git a/htfs/directory.go b/htfs/directory.go index 9fd443cf..e1b0735e 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -184,7 +184,7 @@ func (it *Root) HolotreeBase() string { } func (it *Root) Signature() uint64 { - return sipit([]byte(strings.ToLower(fmt.Sprintf("%s %q", it.Platform, it.Path)))) + return common.Sipit([]byte(strings.ToLower(fmt.Sprintf("%s %q", it.Platform, it.Path)))) } func (it *Root) Rewrite() []byte { diff --git a/htfs/library.go b/htfs/library.go index 3a9167e2..3ab08077 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -70,6 +70,7 @@ type MutableLibrary interface { Record([]byte) error Stage() string CatalogPath(string) string + WriteIdentity([]byte) error } type hololib struct { @@ -105,6 +106,11 @@ func (it *hololib) Identity() string { return fmt.Sprintf("h%s_%st", common.UserHomeIdentity(), suffix[:14]) } +func (it *hololib) WriteIdentity(yaml []byte) error { + markerFile := filepath.Join(it.Stage(), "identity.yaml") + return pathlib.WriteFile(markerFile, yaml, 0o644) +} + func (it *hololib) Stage() string { stage := filepath.Join(common.HolotreeLocation(), it.Identity()) err := os.MkdirAll(stage, 0o755) @@ -212,8 +218,13 @@ func (it *hololib) Export(catalogs, known []string, archive string) (err error) func (it *hololib) Record(blueprint []byte) error { defer common.Stopwatch("Holotree recording took:").Debug() - key := BlueprintHash(blueprint) - common.Timeline("holotree record start %s", key) + err := it.WriteIdentity(blueprint) + if err != nil { + return err + } + key := common.BlueprintHash(blueprint) + common.TimelineBegin("holotree record start %s", key) + defer common.TimelineEnd() fs, err := NewRoot(it.Stage()) if err != nil { return err @@ -256,7 +267,7 @@ func (it *hololib) ValidateBlueprint(blueprint []byte) error { } func (it *hololib) HasBlueprint(blueprint []byte) bool { - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) found, ok := it.queryCache[key] if !ok { found = it.queryBlueprint(key) @@ -301,8 +312,8 @@ func CatalogNames() []string { } func ControllerSpaceName(client, tag []byte) string { - prefix := textual(sipit(client), 7) - suffix := textual(sipit(tag), 8) + prefix := common.Textual(common.Sipit(client), 7) + suffix := common.Textual(common.Sipit(tag), 8) return common.UserHomeIdentity() + "_" + prefix + "_" + suffix } @@ -314,7 +325,7 @@ func touchUsedHash(hash string) { func (it *hololib) TargetDir(blueprint, controller, space []byte) (result string, err error) { defer fail.Around(&err) - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) catalog := it.CatalogPath(key) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) @@ -334,7 +345,7 @@ func UserHolotreeLockfile() string { func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) catalog := it.CatalogPath(key) common.TimelineBegin("holotree space restore start [%s]", key) defer common.TimelineEnd() @@ -401,22 +412,6 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er return targetdir, nil } -func BlueprintHash(blueprint []byte) string { - return textual(sipit(blueprint), 0) -} - -func sipit(key []byte) uint64 { - return common.Siphash(9007199254740993, 2147483647, key) -} - -func textual(key uint64, size int) string { - text := fmt.Sprintf("%016x", key) - if size > 0 { - return text[:size] - } - return text -} - func makedirs(prefix string, suffixes ...string) error { if common.Liveonly { return nil @@ -439,7 +434,7 @@ func New() (MutableLibrary, error) { basedir := common.RobocorpHome() identity := strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)) return &hololib{ - identity: sipit([]byte(identity)), + identity: common.Sipit([]byte(identity)), basedir: basedir, queryCache: make(map[string]bool), }, nil diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index 5ac50d19..75785beb 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -34,6 +34,10 @@ func (it *unmanaged) Stage() string { return it.delegate.Stage() } +func (it *unmanaged) WriteIdentity([]byte) error { + return fmt.Errorf("Not supported yet on virtual holotree.") +} + func (it *unmanaged) CatalogPath(key string) string { return "Unmanaged Does Not Support Catalog Path Request" } @@ -50,7 +54,7 @@ func (it *unmanaged) resolve(blueprint []byte) error { if it.resolved { return nil } - defer common.Log("%sThis is unmanaged holotree space, checking suitability for blueprint: %v%s", pretty.Magenta, BlueprintHash(blueprint), pretty.Reset) + defer common.Log("%sThis is unmanaged holotree space, checking suitability for blueprint: %v%s", pretty.Magenta, common.BlueprintHash(blueprint), pretty.Reset) controller := []byte(common.ControllerIdentity()) space := []byte(common.HolotreeSpace) path, err := it.TargetDir(blueprint, controller, space) @@ -68,8 +72,8 @@ func (it *unmanaged) resolve(blueprint []byte) error { if err != nil { return nil } - expected := BlueprintHash(blueprint) - actual := BlueprintHash(identity) + expected := common.BlueprintHash(blueprint) + actual := common.BlueprintHash(identity) if actual != expected { it.protected = true it.resolved = true diff --git a/htfs/virtual.go b/htfs/virtual.go index 615fb943..59e9e3cb 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -21,7 +21,7 @@ type virtual struct { func Virtual() MutableLibrary { return &virtual{ - identity: sipit([]byte(common.RobocorpHome())), + identity: common.Sipit([]byte(common.RobocorpHome())), } } @@ -51,10 +51,14 @@ func (it *virtual) Export([]string, []string, string) error { return fmt.Errorf("Not supported yet on virtual holotree.") } +func (it *virtual) WriteIdentity([]byte) error { + return fmt.Errorf("Not supported yet on virtual holotree.") +} + func (it *virtual) Record(blueprint []byte) (err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree recording took:").Debug() - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) common.Timeline("holotree record start %s (virtual)", key) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage root: %v", err) @@ -79,7 +83,7 @@ func (it *virtual) TargetDir(blueprint, client, tag []byte) (string, error) { func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) common.Timeline("holotree restore start %s (virtual)", key) name := ControllerSpaceName(client, tag) metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) @@ -148,5 +152,5 @@ func (it *virtual) ValidateBlueprint(blueprint []byte) error { } func (it *virtual) HasBlueprint(blueprint []byte) bool { - return it.key == BlueprintHash(blueprint) + return it.key == common.BlueprintHash(blueprint) } diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index cfc5aaa4..e0553616 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -34,7 +34,7 @@ func ZipLibrary(zipfile string) (Library, error) { identity := strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)) return &ziplibrary{ content: content, - identity: sipit([]byte(identity)), + identity: common.Sipit([]byte(identity)), lookup: lookup, }, nil } @@ -44,7 +44,7 @@ func (it *ziplibrary) ValidateBlueprint(blueprint []byte) error { } func (it *ziplibrary) HasBlueprint(blueprint []byte) bool { - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) _, ok := it.lookup[it.CatalogPath(key)] return ok } @@ -80,7 +80,7 @@ func (it *ziplibrary) CatalogPath(key string) string { func (it *ziplibrary) TargetDir(blueprint, client, tag []byte) (path string, err error) { defer fail.Around(&err) - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) name := ControllerSpaceName(client, tag) fs, err := NewRoot(".") fail.On(err != nil, "Failed to create root -> %v", err) @@ -96,7 +96,7 @@ func (it *ziplibrary) TargetDir(blueprint, client, tag []byte) (path string, err func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() - key := BlueprintHash(blueprint) + key := common.BlueprintHash(blueprint) common.Timeline("holotree restore start %s (zip)", key) name := ControllerSpaceName(client, tag) fs, err := NewRoot(".") From 2afc0a6a2f9df31a9f56de91fcbbc6586f943a49 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 5 Jun 2023 10:07:04 +0300 Subject: [PATCH 399/516] Layers restoring (v14.8.1) - added `RCC_HOLOTREE_SPACE_ROOT` to environment variables provided by rcc - saving `rcc_plan.log` into intermediate layers as well (and it is now in memory presentation while building environment) - restoring partial environment from layers and skipping already available layers (but still only behind `--layered` flag) - layers add new Progress step to rcc, now total is 15 steps. Test changed to match that. --- common/logger.go | 4 +- common/version.go | 2 +- conda/robocorp.go | 1 + conda/workflows.go | 180 ++++++++++++++++++++---------- docs/changelog.md | 10 ++ htfs/commands.go | 49 +++++++- htfs/library.go | 21 ++-- htfs/unmanaged.go | 8 +- htfs/virtual.go | 17 +-- htfs/ziplibrary.go | 15 ++- robot_tests/export_holozip.robot | 4 +- robot_tests/fullrun.robot | 20 ++-- robot_tests/unmanaged_space.robot | 52 ++++----- 13 files changed, 252 insertions(+), 131 deletions(-) diff --git a/common/logger.go b/common/logger.go index 26143a22..7d9a7cf0 100644 --- a/common/logger.go +++ b/common/logger.go @@ -104,6 +104,6 @@ func Progress(step int, form string, details ...interface{}) { ProgressMark = time.Now() delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() message := fmt.Sprintf(form, details...) - Log("#### Progress: %02d/14 %s %8.3fs %s", step, Version, delta, message) - Timeline("%d/14 %s", step, message) + Log("#### Progress: %02d/15 %s %8.3fs %s", step, Version, delta, message) + Timeline("%d/15 %s", step, message) } diff --git a/common/version.go b/common/version.go index f8bd32dc..6a59402d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.8.0` + Version = `v14.8.1` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 711a47a8..be94c66e 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -138,6 +138,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), + "RCC_HOLOTREE_SPACE_ROOT="+location, "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), "RCC_EXE="+common.BinRcc(), "RCC_VERSION="+common.Version, diff --git a/conda/workflows.go b/conda/workflows.go index 04ffc34d..7f557a9d 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -19,12 +19,45 @@ import ( "github.com/robocorp/rcc/shell" ) +const ( + SkipNoLayers SkipLayer = iota + SkipMicromambaLayer SkipLayer = iota + SkipPipLayer SkipLayer = iota + SkipPostinstallLayer SkipLayer = iota + SkipError SkipLayer = iota +) + type ( - Recorder interface { + SkipLayer uint8 + Recorder interface { Record([]byte) error } + PlanWriter struct { + filename string + blob []byte + } ) +func NewPlanWriter(filename string) *PlanWriter { + return &PlanWriter{ + filename: filename, + blob: make([]byte, 0, 50000), + } +} + +func (it *PlanWriter) AsText() string { + return string(it.blob) +} + +func (it *PlanWriter) Write(blob []byte) (int, error) { + it.blob = append(it.blob, blob...) + return len(blob), nil +} + +func (it *PlanWriter) Save() error { + return os.WriteFile(it.filename, it.blob, 0o644) +} + func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } @@ -86,20 +119,22 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { return false } -func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, finalEnv *Environment, recorder Recorder) (bool, error) { +func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, skip SkipLayer, finalEnv *Environment, recorder Recorder) (bool, error) { if !MustMicromamba() { return false, fmt.Errorf("Could not get micromamba installed.") } targetFolder := common.StageFolder - common.Debug("=== pre cleanup phase ===") - common.Timeline("pre cleanup phase.") - err := renameRemove(targetFolder) - if err != nil { - return false, err + if skip == SkipNoLayers { + common.Debug("=== pre cleanup phase ===") + common.Timeline("pre cleanup phase.") + err := renameRemove(targetFolder) + if err != nil { + return false, err + } } common.Debug("=== first try phase ===") common.Timeline("first try.") - success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv, recorder) + success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, skip, finalEnv, recorder) if !success && !force && !fatal { journal.CurrentBuildEvent().Rebuild() cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) @@ -107,18 +142,29 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall common.Timeline("second try.") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") - err = renameRemove(targetFolder) + err := renameRemove(targetFolder) if err != nil { return false, err } - success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, finalEnv, recorder) + success, _ = newLiveInternal(yaml, condaYaml, requirementsText, key, true, freshInstall, skip, finalEnv, recorder) } if success { journal.CurrentBuildEvent().Successful() } return success, nil } + +func assertStageFolder(location string) { + base := filepath.Base(location) + holotree := strings.HasPrefix(base, "h") && strings.HasSuffix(base, "t") + virtual := strings.HasPrefix(base, "v") && strings.HasSuffix(base, "h") + if !(holotree || virtual) { + panic(fmt.Sprintf("FATAL: incorrect stage %q for environment building!", location)) + } +} + func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, force bool) (bool, bool) { + assertStageFolder(targetFolder) common.TimelineBegin("Layer: micromamba [%s]", fingerprint) defer common.TimelineEnd() @@ -127,7 +173,7 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. if force { ttl = "0" } - common.Progress(6, "Running micromamba phase. (micromamba v%s) [layer: %s]", MicromambaVersion(), fingerprint) + common.Progress(7, "Running micromamba phase. (micromamba v%s) [layer: %s]", MicromambaVersion(), fingerprint) mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -152,7 +198,8 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. return true, false } -func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool, bool, string) { +func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer) (bool, bool, bool, string) { + assertStageFolder(targetFolder) common.TimelineBegin("Layer: pip [%s]", fingerprint) defer common.TimelineEnd() @@ -165,7 +212,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. pipCache, wheelCache := common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - common.Progress(7, "Skipping pip install phase -- no pip dependencies.") + common.Progress(8, "Skipping pip install phase -- no pip dependencies.") } else { if !pyok { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) @@ -173,7 +220,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) return false, false, pipUsed, "" } - common.Progress(7, "Running pip install phase. (pip v%s) [layer: %s]", PipVersion(python), fingerprint) + common.Progress(8, "Running pip install phase. (pip v%s) [layer: %s]", PipVersion(python), fingerprint) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander(python, "-m", "pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -181,7 +228,6 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip install phase ===") code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) - planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") @@ -195,13 +241,14 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. return true, false, pipUsed, python } -func postInstallLayer(fingerprint string, postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File) (bool, bool) { +func postInstallLayer(fingerprint string, postInstall []string, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer) (bool, bool) { + assertStageFolder(targetFolder) common.TimelineBegin("Layer: post install scripts [%s]", fingerprint) defer common.TimelineEnd() fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - common.Progress(8, "Post install scripts phase started. [layer: %s]", fingerprint) + common.Progress(9, "Post install scripts phase started. [layer: %s]", fingerprint) common.Debug("=== post install phase ===") for _, script := range postInstall { scriptCommand, err := shell.Split(script) @@ -212,7 +259,6 @@ func postInstallLayer(fingerprint string, postInstall []string, targetFolder str } common.Debug("Running post install script '%s' ...", script) _, err = LiveExecution(planWriter, targetFolder, scriptCommand...) - planSink.Sync() if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) @@ -221,13 +267,14 @@ func postInstallLayer(fingerprint string, postInstall []string, targetFolder str } journal.CurrentBuildEvent().PostInstallComplete() } else { - common.Progress(8, "Post install scripts phase skipped -- no scripts.") + common.Progress(9, "Post install scripts phase skipped -- no scripts.") } return true, false } -func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, planSink *os.File, force bool, recorder Recorder) (bool, bool, bool, string) { - common.TimelineBegin("Holotree layers") +func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, targetFolder string, stopwatch fmt.Stringer, planWriter io.Writer, theplan *PlanWriter, force bool, skip SkipLayer, recorder Recorder) (bool, bool, bool, string) { + assertStageFolder(targetFolder) + common.TimelineBegin("Holotree layers at %q", targetFolder) defer common.TimelineEnd() pipNeeded := len(requirementsText) > 0 @@ -236,61 +283,74 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t layers := finalEnv.AsLayers() fingerprints := finalEnv.FingerprintLayers() - success, fatal := micromambaLayer(fingerprints[0], condaYaml, targetFolder, stopwatch, planWriter, force) - if !success { - return success, fatal, false, "" - } - if common.LayeredHolotree && (pipNeeded || postInstall) { - recorder.Record([]byte(layers[0])) - } - success, fatal, pipUsed, python := pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter, planSink) - if !success { - return success, fatal, pipUsed, python + var success, fatal, pipUsed bool + var python string + + if skip < SkipMicromambaLayer { + success, fatal = micromambaLayer(fingerprints[0], condaYaml, targetFolder, stopwatch, planWriter, force) + if !success { + return success, fatal, false, "" + } + if common.LayeredHolotree && (pipNeeded || postInstall) { + fmt.Fprintf(theplan, "\n--- micromamba layer complete [on layerd holotree] ---\n\n") + common.Error("saving rcc_plan.log", theplan.Save()) + recorder.Record([]byte(layers[0])) + } + } else { + common.Progress(7, "Skipping micromamba phase, layer exists.") } - if common.LayeredHolotree && pipUsed && postInstall { - recorder.Record([]byte(layers[1])) + if skip < SkipPipLayer { + success, fatal, pipUsed, python = pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter) + if !success { + return success, fatal, pipUsed, python + } + if common.LayeredHolotree && pipUsed && postInstall { + fmt.Fprintf(theplan, "\n--- pip layer complete [on layerd holotree] ---\n\n") + common.Error("saving rcc_plan.log", theplan.Save()) + recorder.Record([]byte(layers[1])) + } + } else { + common.Progress(8, "Skipping pip phase, layer exists.") } - success, fatal = postInstallLayer(fingerprints[2], finalEnv.PostInstall, targetFolder, stopwatch, planWriter, planSink) - if !success { - return success, fatal, pipUsed, python + if skip < SkipPostinstallLayer { + success, fatal = postInstallLayer(fingerprints[2], finalEnv.PostInstall, targetFolder, stopwatch, planWriter) + if !success { + return success, fatal, pipUsed, python + } + } else { + common.Progress(9, "Skipping post install scripts phase, layer exists.") } return true, false, pipUsed, python } -func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, finalEnv *Environment, recorder Recorder) (bool, bool) { +func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, skip SkipLayer, finalEnv *Environment, recorder Recorder) (bool, bool) { targetFolder := common.StageFolder - planfile := fmt.Sprintf("%s.plan", targetFolder) - planSink, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) - if err != nil { - return false, false - } + theplan := NewPlanWriter(filepath.Join(targetFolder, "rcc_plan.log")) + failure := true defer func() { - planSink.Close() - content, err := os.ReadFile(planfile) - if err == nil { - common.Log("%s", string(content)) + if failure { + common.Log("%s", theplan.AsText()) } - os.Remove(planfile) }() planalyzer := NewPlanAnalyzer(true) defer planalyzer.Close() - planWriter := io.MultiWriter(planSink, planalyzer) + planWriter := io.MultiWriter(theplan, planalyzer) fmt.Fprintf(planWriter, "--- installation plan %q %s [force: %v, fresh: %v| rcc %s] ---\n\n", key, time.Now().Format(time.RFC3339), force, freshInstall, common.Version) stopwatch := common.Stopwatch("installation plan") fmt.Fprintf(planWriter, "--- plan blueprint @%ss ---\n\n", stopwatch) fmt.Fprintf(planWriter, "%s\n", yaml) - success, fatal, pipUsed, python := holotreeLayers(condaYaml, requirementsText, finalEnv, targetFolder, stopwatch, planWriter, planSink, force, recorder) + success, fatal, pipUsed, python := holotreeLayers(condaYaml, requirementsText, finalEnv, targetFolder, stopwatch, planWriter, theplan, force, skip, recorder) if !success { return success, fatal } - common.Progress(9, "Activate environment started phase.") + common.Progress(10, "Activate environment started phase.") common.Debug("=== activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) - err = Activate(planWriter, targetFolder) + err := Activate(planWriter, targetFolder) if err != nil { common.Log("%sActivation failure: %v%s", pretty.Yellow, err, pretty.Reset) } @@ -303,12 +363,11 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- pip check plan @%ss ---\n\n", stopwatch) if common.StrictFlag && pipUsed { - common.Progress(10, "Running pip check phase.") + common.Progress(11, "Running pip check phase.") pipCommand := common.NewCommander(python, "-m", "pip", "check", "--no-color") pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") code, err := LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) - planSink.Sync() if err != nil || code != 0 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pipcheck", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip check fail.") @@ -317,16 +376,15 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } common.Timeline("pip check done.") } else { - common.Progress(10, "Pip check skipped.") + common.Progress(11, "Pip check skipped.") } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) - planSink.Sync() - planSink.Close() - common.Progress(11, "Update installation plan.") - finalplan := filepath.Join(targetFolder, "rcc_plan.log") - os.Rename(planfile, finalplan) + common.Progress(12, "Update installation plan.") + common.Error("saving rcc_plan.log", theplan.Save()) common.Debug("=== finalize phase ===") + failure = false + return true, false } @@ -371,7 +429,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. return hash, yaml, right, err } -func LegacyEnvironment(recorder Recorder, force bool, configurations ...string) error { +func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configurations ...string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() @@ -396,7 +454,7 @@ func LegacyEnvironment(recorder Recorder, force bool, configurations ...string) defer os.Remove(condaYaml) defer os.Remove(requirementsText) - success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv, recorder) + success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, skip, finalEnv, recorder) if err != nil { return err } diff --git a/docs/changelog.md b/docs/changelog.md index e3e7aa07..271914cd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # rcc change log +## v14.8.1 (date: 5.6.2023) UNSTABLE + +- added `RCC_HOLOTREE_SPACE_ROOT` to environment variables provided by rcc +- saving `rcc_plan.log` into intermediate layers as well (and it is now in + memory presentation while building environment) +- restoring partial environment from layers and skipping already available + layers (but still only behind `--layered` flag) +- layers add new Progress step to rcc, now total is 15 steps. Test changed + to match that. + ## v14.8.0 (date: 31.5.2023) UNSTABLE - support for separating layers and calculating their fingerprints diff --git a/htfs/commands.go b/htfs/commands.go index 82cb8304..82397856 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -35,7 +35,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal path := "" defer func() { - common.Progress(14, "Fresh holotree done [with %d workers].", anywork.Scale()) + common.Progress(15, "Fresh holotree done [with %d workers].", anywork.Scale()) if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) } @@ -93,12 +93,12 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } if restore { - common.Progress(13, "Restore space from library [with %d workers].", anywork.Scale()) + common.Progress(14, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() } else { - common.Progress(13, "Restoring space skipped.") + common.Progress(14, "Restoring space skipped.") } return path, scorecard, nil @@ -151,16 +151,28 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) - common.Progress(5, "Build environment into holotree stage.") + common.Progress(5, "Build environment into holotree stage %q.", tree.Stage()) identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = os.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) - err = conda.LegacyEnvironment(tree, force, identityfile) + + skip := conda.SkipNoLayers + if !force && common.LayeredHolotree { + common.Progress(6, "Restore partial environment into holotree stage %q.", tree.Stage()) + skip = RestoreLayersTo(tree, identityfile, tree.Stage()) + } else { + common.Progress(6, "Restore partial environment skipped. Layers disabled or force used.") + } + + err = os.WriteFile(identityfile, blueprint, 0o644) + fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) + + err = conda.LegacyEnvironment(tree, force, skip, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) scorecard.Midpoint() - common.Progress(12, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) + common.Progress(13, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) journal.CurrentBuildEvent().RecordComplete() @@ -169,6 +181,31 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec return nil } +func RestoreLayersTo(tree MutableLibrary, identityfile string, targetDir string) conda.SkipLayer { + config, err := conda.ReadCondaYaml(identityfile) + if err != nil { + return conda.SkipNoLayers + } + + layers := config.AsLayers() + mambaLayer := []byte(layers[0]) + pipLayer := []byte(layers[1]) + base := filepath.Base(targetDir) + if tree.HasBlueprint(pipLayer) { + _, err = tree.RestoreTo(pipLayer, base, common.ControllerIdentity(), common.HolotreeSpace, true) + if err == nil { + return conda.SkipPipLayer + } + } + if tree.HasBlueprint(mambaLayer) { + _, err = tree.RestoreTo(mambaLayer, base, common.ControllerIdentity(), common.HolotreeSpace, true) + if err == nil { + return conda.SkipMicromambaLayer + } + } + return conda.SkipNoLayers +} + func RobotBlueprints(userBlueprints []string, packfile string) (robot.Robot, []string) { var err error var config robot.Robot diff --git a/htfs/library.go b/htfs/library.go index 3ab08077..8a316b2d 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -57,6 +57,7 @@ type Library interface { Open(string) (io.Reader, Closer, error) TargetDir([]byte, []byte, []byte) (string, error) Restore([]byte, []byte, []byte) (string, error) + RestoreTo([]byte, string, string, string, bool) (string, error) } type MutableLibrary interface { @@ -343,20 +344,24 @@ func UserHolotreeLockfile() string { } func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { + return it.RestoreTo(blueprint, ControllerSpaceName(client, tag), string(client), string(tag), false) +} + +func (it *hololib) RestoreTo(blueprint []byte, label, controller, space string, partial bool) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() + key := common.BlueprintHash(blueprint) catalog := it.CatalogPath(key) common.TimelineBegin("holotree space restore start [%s]", key) defer common.TimelineEnd() - name := ControllerSpaceName(client, tag) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) err = fs.LoadFrom(catalog) fail.On(err != nil, "Failed to load catalog %s -> %v", catalog, err) - metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(fs.HolotreeBase(), name) - lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + targetdir := filepath.Join(fs.HolotreeBase(), label) + metafile := fmt.Sprintf("%s.meta", targetdir) + lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() @@ -395,17 +400,17 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) journal.CurrentBuildEvent().Dirty(score.Dirtyness()) - fs.Controller = string(client) - fs.Space = string(tag) + fs.Controller = controller + fs.Space = space err = fs.SaveAs(metafile) fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) pathlib.TouchWhen(catalog, time.Now()) planfile := filepath.Join(targetdir, "rcc_plan.log") - if pathlib.FileExist(planfile) { + if !partial && pathlib.FileExist(planfile) { common.Log("%sInstallation plan is: %v%s", pretty.Yellow, planfile, pretty.Reset) } identityfile := filepath.Join(targetdir, "identity.yaml") - if pathlib.FileExist(identityfile) { + if !partial && pathlib.FileExist(identityfile) { common.Log("%sEnvironment configuration descriptor is: %v%s", pretty.Yellow, identityfile, pretty.Reset) } touchUsedHash(key) diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index 75785beb..728b9cf4 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -109,10 +109,14 @@ func (it *unmanaged) TargetDir(blueprint, client, tag []byte) (string, error) { return it.delegate.TargetDir(blueprint, client, tag) } -func (it *unmanaged) Restore(blueprint, client, tag []byte) (string, error) { +func (it *unmanaged) Restore(blueprint, client, tag []byte) (result string, err error) { + return it.RestoreTo(blueprint, ControllerSpaceName(client, tag), string(client), string(tag), false) +} + +func (it *unmanaged) RestoreTo(blueprint []byte, label, controller, space string, partial bool) (result string, err error) { it.resolve(blueprint) if !it.protected { - return it.delegate.Restore(blueprint, client, tag) + return it.delegate.RestoreTo(blueprint, label, controller, space, partial) } common.Timeline("holotree unmanaged restore prevention") if len(it.path) > 0 { diff --git a/htfs/virtual.go b/htfs/virtual.go index 59e9e3cb..2a1dafd0 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -81,14 +81,17 @@ func (it *virtual) TargetDir(blueprint, client, tag []byte) (string, error) { return filepath.Join(common.HolotreeLocation(), name), nil } -func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { +func (it *virtual) Restore(blueprint, client, tag []byte) (result string, err error) { + return it.RestoreTo(blueprint, ControllerSpaceName(client, tag), string(client), string(tag), false) +} + +func (it *virtual) RestoreTo(blueprint []byte, label, controller, space string, partial bool) (result string, err error) { defer common.Stopwatch("Holotree restore took:").Debug() key := common.BlueprintHash(blueprint) common.Timeline("holotree restore start %s (virtual)", key) - name := ControllerSpaceName(client, tag) - metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(common.HolotreeLocation(), name) - lockfile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) + targetdir := filepath.Join(common.HolotreeLocation(), label) + metafile := fmt.Sprintf("%s.meta", targetdir) + lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree virtual lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() @@ -126,8 +129,8 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) journal.CurrentBuildEvent().Dirty(score.Dirtyness()) - fs.Controller = string(client) - fs.Space = string(tag) + fs.Controller = controller + fs.Space = space err = fs.SaveAs(metafile) if err != nil { return "", err diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index e0553616..58dde376 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -94,11 +94,14 @@ func (it *ziplibrary) TargetDir(blueprint, client, tag []byte) (path string, err } func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { + return it.RestoreTo(blueprint, ControllerSpaceName(client, tag), string(client), string(tag), false) +} + +func (it *ziplibrary) RestoreTo(blueprint []byte, label, controller, space string, partial bool) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() key := common.BlueprintHash(blueprint) common.Timeline("holotree restore start %s (zip)", key) - name := ControllerSpaceName(client, tag) fs, err := NewRoot(".") fail.On(err != nil, "Failed to create root -> %v", err) catalog := it.CatalogPath(key) @@ -107,9 +110,9 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err defer closer() err = fs.ReadFrom(reader) fail.On(err != nil, "Failed to read catalog %q -> %v", catalog, err) - metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(fs.HolotreeBase(), name) - lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + targetdir := filepath.Join(fs.HolotreeBase(), label) + metafile := fmt.Sprintf("%s.meta", targetdir) + lockfile := fmt.Sprintf("%s.lck", targetdir) completed := pathlib.LockWaitMessage(lockfile, "Serialized holotree restore [holotree base lock]") locker, err := pathlib.Locker(lockfile, 30000) completed() @@ -140,8 +143,8 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err defer common.Timeline("- dirty %d/%d", score.dirty, score.total) common.Debug("Holotree dirty workload: %d/%d\n", score.dirty, score.total) journal.CurrentBuildEvent().Dirty(score.Dirtyness()) - fs.Controller = string(client) - fs.Space = string(tag) + fs.Controller = controller + fs.Space = space err = fs.SaveAs(metafile) fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) return targetdir, nil diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 8ea6c1a8..b4c627bf 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -35,8 +35,8 @@ Goal: Create environment for standalone robot Must Have RCC_INSTALLATION_ID= Must Have 4e67cd8_fcb4b859 Use STDERR - Must Have Progress: 01/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 15/15 Goal: Must have author space visible Step build/rcc ht ls diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index c397e7ee..dc803b83 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -75,9 +75,9 @@ Goal: Run task in place in debug mode and with timeline. Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR - Must Have Progress: 01/14 - Must Have Progress: 02/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 02/15 + Must Have Progress: 15/15 Must Have rpaframework Must Have PID # Must Have [N] @@ -108,13 +108,13 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 01/14 - Wont Have Progress: 03/14 - Wont Have Progress: 05/14 - Wont Have Progress: 07/14 - Wont Have Progress: 09/14 - Must Have Progress: 13/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Wont Have Progress: 03/15 + Wont Have Progress: 05/15 + Wont Have Progress: 07/15 + Wont Have Progress: 09/15 + Must Have Progress: 14/15 + Must Have Progress: 15/15 Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/unmanaged_space.robot b/robot_tests/unmanaged_space.robot index 891bb550..c31c9bd0 100644 --- a/robot_tests/unmanaged_space.robot +++ b/robot_tests/unmanaged_space.robot @@ -32,13 +32,13 @@ Goal: See variables from specific unamanged space Wont Have ROBOT_ARTIFACTS= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/14 - Must Have Progress: 02/14 - Must Have Progress: 04/14 - Must Have Progress: 05/14 - Must Have Progress: 06/14 - Must Have Progress: 13/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 02/15 + Must Have Progress: 04/15 + Must Have Progress: 05/15 + Must Have Progress: 06/15 + Must Have Progress: 14/15 + Must Have Progress: 15/15 Goal: Wont allow use of unmanaged space with incompatible conda.yaml Step build/rcc holotree variables --debug --unmanaged --space python39 --controller citests robot_tests/python375.yaml 6 @@ -48,14 +48,14 @@ Goal: Wont allow use of unmanaged space with incompatible conda.yaml Wont Have RCC_INSTALLATION_ID= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/14 - Must Have Progress: 02/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 02/15 + Must Have Progress: 15/15 - Wont Have Progress: 04/14 - Wont Have Progress: 05/14 - Wont Have Progress: 06/14 - Wont Have Progress: 13/14 + Wont Have Progress: 04/15 + Wont Have Progress: 05/15 + Wont Have Progress: 06/15 + Wont Have Progress: 14/15 Must Have Existing unmanaged space fingerprint Must Have does not match requested one @@ -83,24 +83,24 @@ Goal: Allows different unmanaged space for different conda.yaml Wont Have ROBOT_ARTIFACTS= Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/14 - Must Have Progress: 02/14 - Must Have Progress: 04/14 - Must Have Progress: 05/14 - Must Have Progress: 06/14 - Must Have Progress: 13/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 02/15 + Must Have Progress: 04/15 + Must Have Progress: 05/15 + Must Have Progress: 06/15 + Must Have Progress: 14/15 + Must Have Progress: 15/15 Goal: Wont allow use of unmanaged space with incompatible conda.yaml when two unmanaged spaces exists Step build/rcc holotree variables --debug --unmanaged --space python37 --controller citests robot_tests/python3913.yaml 6 Use STDERR Must Have This is unmanaged holotree space - Must Have Progress: 01/14 - Must Have Progress: 02/14 - Must Have Progress: 14/14 + Must Have Progress: 01/15 + Must Have Progress: 02/15 + Must Have Progress: 15/15 - Wont Have Progress: 05/14 - Wont Have Progress: 13/14 + Wont Have Progress: 05/15 + Wont Have Progress: 14/15 Must Have Existing unmanaged space fingerprint Must Have does not match requested one From 2a0e03039e902116ab06a7ce63192ab861261763 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 6 Jun 2023 12:31:30 +0300 Subject: [PATCH 400/516] Layers bug fixes and other improvements (v14.8.2) - added missing golden yaml file saving on layers - added worker count on second progress indicator - reporting relative time ratios on setup/run balances - fixed bug in buildstats, where it was using global variables (instead of "it") --- common/algorithms.go | 10 ++++++ common/algorithms_test.go | 11 ++++++ common/version.go | 2 +- conda/workflows.go | 2 ++ docs/changelog.md | 7 ++++ htfs/commands.go | 2 +- journal/buildstats.go | 76 ++++++++++++++++++++++----------------- operations/running.go | 4 +-- pretty/functions.go | 5 +++ robot_tests/fullrun.robot | 3 +- 10 files changed, 83 insertions(+), 39 deletions(-) diff --git a/common/algorithms.go b/common/algorithms.go index 46304ca2..133e7cbe 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -80,3 +80,13 @@ func Textual(key uint64, size int) string { } return text } + +func Gcd(left, right int64) int64 { + for left != 0 { + left, right = right%left, left + } + if right == 0 { + return 1 + } + return right +} diff --git a/common/algorithms_test.go b/common/algorithms_test.go index 5972bd83..947c1cf1 100644 --- a/common/algorithms_test.go +++ b/common/algorithms_test.go @@ -23,3 +23,14 @@ func TestCanCallEntropyFunction(t *testing.T) { must_be.True(between(0.5, common.Entropy([]byte("abcdefghijklmnopqrstuvwxyz")), 0.6)) must_be.True(between(0.43, common.Entropy([]byte("edf3419283feac3d4f8bb34aa9")), 0.44)) } + +func TestCanCalculateGcd(t *testing.T) { + must_be, _ := hamlet.Specifications(t) + + must_be.Equal(int64(5), common.Gcd(15, 20)) + must_be.Equal(int64(1), common.Gcd(3, 5)) + must_be.Equal(int64(5), common.Gcd(5, 5)) + must_be.Equal(int64(5), common.Gcd(0, 5)) + must_be.Equal(int64(5), common.Gcd(5, 0)) + must_be.Equal(int64(1), common.Gcd(0, 0)) +} diff --git a/common/version.go b/common/version.go index 6a59402d..92f56d69 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.8.1` + Version = `v14.8.2` ) diff --git a/conda/workflows.go b/conda/workflows.go index 7f557a9d..fdfa8789 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -294,6 +294,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t if common.LayeredHolotree && (pipNeeded || postInstall) { fmt.Fprintf(theplan, "\n--- micromamba layer complete [on layerd holotree] ---\n\n") common.Error("saving rcc_plan.log", theplan.Save()) + common.Error("saving golden master", goldenMaster(targetFolder, false)) recorder.Record([]byte(layers[0])) } } else { @@ -307,6 +308,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t if common.LayeredHolotree && pipUsed && postInstall { fmt.Fprintf(theplan, "\n--- pip layer complete [on layerd holotree] ---\n\n") common.Error("saving rcc_plan.log", theplan.Save()) + common.Error("saving golden master", goldenMaster(targetFolder, true)) recorder.Record([]byte(layers[1])) } } else { diff --git a/docs/changelog.md b/docs/changelog.md index 271914cd..16c21e89 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v14.8.2 (date: 6.6.2023) UNSTABLE + +- added missing golden yaml file saving on layers +- added worker count on second progress indicator +- reporting relative time ratios on setup/run balances +- fixed bug in buildstats, where it was using global variables (instead of "it") + ## v14.8.1 (date: 5.6.2023) UNSTABLE - added `RCC_HOLOTREE_SPACE_ROOT` to environment variables provided by rcc diff --git a/htfs/commands.go b/htfs/commands.go index 82397856..cdb80758 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -64,7 +64,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false - common.Progress(2, "Holotree blueprint is %q [%s].", common.EnvironmentHash, common.Platform()) + common.Progress(2, "Holotree blueprint is %q [%s with %d workers].", common.EnvironmentHash, common.Platform(), anywork.Scale()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() diff --git a/journal/buildstats.go b/journal/buildstats.go index 9b2e9283..b3d8291d 100644 --- a/journal/buildstats.go +++ b/journal/buildstats.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "math" "os" "path/filepath" "sort" @@ -70,6 +71,17 @@ func init() { buildevent = NewBuildEvent() } +func CurrentBuildEvent() *BuildEvent { + return buildevent +} + +func BuildEventStats(label string) { + err := serialize(buildevent.finished(label)) + if err != nil { + pretty.Warning("build stats for %q failed, reason: %v", label, err) + } +} + func asPercent(value float64) string { return fmt.Sprintf("%5.1f%%", value) } @@ -363,10 +375,6 @@ func tabs(columns ...any) []byte { return []byte(fmt.Sprintf(form, columns...)) } -func CurrentBuildEvent() *BuildEvent { - return buildevent -} - func BuildEventFilenameFor(stamp time.Time) string { year, week := stamp.ISOWeek() filename := fmt.Sprintf("stats_%s_%04d_%02d.log", common.UserHomeIdentity(), year, week) @@ -389,13 +397,6 @@ func BuildEventFilenamesFor(weekcount int) []string { return result } -func BuildEventStats(label string) { - err := serialize(buildevent.finished(label)) - if err != nil { - pretty.Warning("build stats for %q failed, reason: %v", label, err) - } -} - func serialize(event *BuildEvent) (err error) { defer fail.Around(&err) @@ -428,64 +429,73 @@ func (it *BuildEvent) Successful() { } func (it *BuildEvent) StartNow(force bool) { - buildevent.Started = it.stowatch() - buildevent.Force = force + it.Started = it.stowatch() + it.Force = force } func (it *BuildEvent) Blueprint(blueprint string) { - buildevent.BlueprintHash = blueprint + it.BlueprintHash = blueprint } func (it *BuildEvent) Rebuild() { - buildevent.Retry = true - buildevent.Build = true + it.Retry = true + it.Build = true } func (it *BuildEvent) PrepareComplete() { - buildevent.Build = true - buildevent.Prepared = it.stowatch() + it.Build = true + it.Prepared = it.stowatch() } func (it *BuildEvent) MicromambaComplete() { - buildevent.Build = true - buildevent.MicromambaDone = it.stowatch() + it.Build = true + it.MicromambaDone = it.stowatch() } func (it *BuildEvent) PipComplete() { - buildevent.Build = true - buildevent.PipDone = it.stowatch() + it.Build = true + it.PipDone = it.stowatch() } func (it *BuildEvent) PostInstallComplete() { - buildevent.Build = true - buildevent.PostInstallDone = it.stowatch() + it.Build = true + it.PostInstallDone = it.stowatch() } func (it *BuildEvent) RecordComplete() { - buildevent.RecordDone = it.stowatch() + it.RecordDone = it.stowatch() } func (it *BuildEvent) Dirty(dirtyness float64) { - buildevent.Dirtyness = dirtyness + it.Dirtyness = dirtyness } func (it *BuildEvent) RestoreComplete() { - buildevent.RestoreDone = it.stowatch() + it.RestoreDone = it.stowatch() } func (it *BuildEvent) PreRunComplete() { - buildevent.Run = true - buildevent.PreRunDone = it.stowatch() + it.Run = true + it.PreRunDone = it.stowatch() } func (it *BuildEvent) RobotStarts() { - buildevent.Run = true - buildevent.RobotStart = it.stowatch() + it.Run = true + it.RobotStart = it.stowatch() } func (it *BuildEvent) RobotEnds() { - buildevent.Run = true - buildevent.RobotEnd = it.stowatch() + it.Run = true + it.RobotEnd = it.stowatch() + reportRatio("Build/Pre-run", it.first(prerun, restore)-it.Started, it.first(prerun, restore)-it.RestoreDone) + reportRatio("Setup/Run", it.first(prerun, restore)-it.Started, it.RobotEnd-it.first(prerun, restore)) +} + +func reportRatio(label string, first, second float64) { + left := int64(math.Ceil(10 * first)) + right := int64(math.Ceil(10 * second)) + gcd := common.Gcd(left, right) + pretty.Lowlight(" | %q relative time allocation ratio: %d:%d", label, left/gcd, right/gcd) } func (it *BuildEvent) first(tools ...picker) float64 { diff --git a/operations/running.go b/operations/running.go index abf1b93d..122aa189 100644 --- a/operations/running.go +++ b/operations/running.go @@ -180,8 +180,8 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. } func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { - common.Timeline("robot execution starts (simple=%v).", simple) - defer common.Timeline("robot execution done.") + common.TimelineBegin("robot execution (simple=%v).", simple) + defer common.TimelineEnd() pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) if simple { ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) diff --git a/pretty/functions.go b/pretty/functions.go index 1764f99a..53f29f0f 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -26,6 +26,11 @@ func Highlight(format string, rest ...interface{}) { common.Log(niceform, rest...) } +func Lowlight(format string, rest ...interface{}) { + niceform := fmt.Sprintf("%s%s%s%s", Grey, Faint, format, Reset) + common.Log(niceform, rest...) +} + func Exit(code int, format string, rest ...interface{}) { var niceform string if code == 0 { diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index dc803b83..297814ba 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -89,8 +89,7 @@ Goal: Run task in place in debug mode and with timeline. Must Have Installation plan is: Must Have Command line is: [ Must Have rcc timeline - Must Have robot execution starts (simple=false). - Must Have robot execution done. + Must Have robot execution (simple=false). Must Have Now. Must Have Wanted Must Have Available From f843997df89d3ff914a0ffd276be4444f148ca09 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Jun 2023 08:55:30 +0300 Subject: [PATCH 401/516] Shared ROBOCORP_HOME detection (v14.9.0) - added one user per `ROBOCORP_HOME` warnings - added also diagnostics to warn about above issue - full cleanup now also removes `rcccache.yaml` file - removed "Robots" section from `rcccache.yaml` file --- cmd/rcc/main.go | 33 +++++++++++++++++++++++++++++++++ common/categories.go | 23 ++++++++++++----------- common/version.go | 2 +- conda/cleanup.go | 3 +++ docs/changelog.md | 7 +++++++ operations/cache.go | 8 ++++---- operations/diagnostics.go | 22 +++++++++++++++++++++- 7 files changed, 81 insertions(+), 17 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 9309bd95..d37d1534 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -1,9 +1,12 @@ package main import ( + "fmt" "os" + "os/user" "path/filepath" "runtime" + "strings" "time" "github.com/robocorp/rcc/anywork" @@ -13,6 +16,8 @@ import ( "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" ) const ( @@ -25,6 +30,29 @@ var ( markedAlready = false ) +func EnsureUserRegistered() (string, error) { + var warning string + + cache, err := operations.SummonCache() + if err != nil { + return warning, err + } + who, err := user.Current() + if err != nil { + return warning, err + } + updated, ok := set.Update(cache.Users, who.Username) + size := len(updated) + if size > 1 { + warning = fmt.Sprintf("More than one user is using same ROBOCORP_HOME location! Those users are: %s!", strings.Join(updated, ", ")) + } + if !ok { + return warning, nil + } + cache.Users = updated + return warning, cache.Save() +} + func TimezoneMetric() error { cache, err := operations.SummonCache() if err != nil { @@ -96,6 +124,11 @@ func markTempForRecycling() { func main() { defer ExitProtection() + warning, _ := EnsureUserRegistered() + if len(warning) > 0 { + defer pretty.Warning("%s", warning) + } + anywork.Backlog(conda.BugsCleanup) if common.SharedHolotree { diff --git a/common/categories.go b/common/categories.go index dfd81e23..4ca94cb6 100644 --- a/common/categories.go +++ b/common/categories.go @@ -1,15 +1,16 @@ package common const ( - CategoryUndefined = 0 - CategoryLongPath = 1010 - CategoryLockFile = 1020 - CategoryLockPid = 1021 - CategoryPathCheck = 1030 - CategoryHolotreeShared = 2010 - CategoryRobocorpHome = 3010 - CategoryNetworkDNS = 4010 - CategoryNetworkLink = 4020 - CategoryNetworkHEAD = 4030 - CategoryNetworkCanary = 4040 + CategoryUndefined = 0 + CategoryLongPath = 1010 + CategoryLockFile = 1020 + CategoryLockPid = 1021 + CategoryPathCheck = 1030 + CategoryHolotreeShared = 2010 + CategoryRobocorpHome = 3010 + CategoryRobocorpHomeMembers = 3020 + CategoryNetworkDNS = 4010 + CategoryNetworkLink = 4020 + CategoryNetworkHEAD = 4030 + CategoryNetworkCanary = 4040 ) diff --git a/common/version.go b/common/version.go index 92f56d69..39477df0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.8.2` + Version = `v14.9.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index c9609d99..cb029d1f 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -99,9 +99,11 @@ func spotlessCleanup(dryrun bool) error { if err != nil { return err } + rcccache := filepath.Join(common.RobocorpHome(), "rcccache.yaml") if dryrun { common.Log("- %v", BinMicromamba()) common.Log("- %v", common.RobotCache()) + common.Log("- %v", rcccache) common.Log("- %v", common.OldEventJournal()) common.Log("- %v", common.JournalLocation()) common.Log("- %v", common.HololibCatalogLocation()) @@ -110,6 +112,7 @@ func spotlessCleanup(dryrun bool) error { } safeRemove("executable", BinMicromamba()) safeRemove("cache", common.RobotCache()) + safeRemove("cache", rcccache) safeRemove("old", common.OldEventJournal()) safeRemove("journals", common.JournalLocation()) safeRemove("catalogs", common.HololibCatalogLocation()) diff --git a/docs/changelog.md b/docs/changelog.md index 16c21e89..79e57ff8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v14.9.0 (date: 7.6.2023) + +- added one user per `ROBOCORP_HOME` warnings +- added also diagnostics to warn about above issue +- full cleanup now also removes `rcccache.yaml` file +- removed "Robots" section from `rcccache.yaml` file + ## v14.8.2 (date: 6.6.2023) UNSTABLE - added missing golden yaml file saving on layers diff --git a/operations/cache.go b/operations/cache.go index b150bdd0..a76028fa 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -31,21 +31,21 @@ type CredentialMap map[string]*Credential type StampMap map[string]int64 type Cache struct { - Robots FolderMap `yaml:"robots"` Credentials CredentialMap `yaml:"credentials"` Stamps StampMap `yaml:"stamps"` + Users []string `yaml:"users"` } func (it Cache) Ready() *Cache { - if it.Robots == nil { - it.Robots = make(FolderMap) - } if it.Credentials == nil { it.Credentials = make(CredentialMap) } if it.Stamps == nil { it.Stamps = make(StampMap) } + if it.Users == nil { + it.Users = []string{} + } return &it } diff --git a/operations/diagnostics.go b/operations/diagnostics.go index a5a980dc..a6314d08 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -119,7 +119,11 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Checks = append(result.Checks, verifySharedDirectory(common.HololibLibraryLocation())) } result.Checks = append(result.Checks, robocorpHomeCheck()) - check := workdirCheck() + check := robocorpHomeMemberCheck() + if check != nil { + result.Checks = append(result.Checks, check) + } + check = workdirCheck() if check != nil { result.Checks = append(result.Checks, check) } @@ -320,6 +324,22 @@ func workdirCheck() *common.DiagnosticCheck { return nil } +func robocorpHomeMemberCheck() *common.DiagnosticCheck { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + cache, err := SummonCache() + if err != nil || len(cache.Users) < 2 { + return nil + } + members := strings.Join(cache.Users, ", ") + return &common.DiagnosticCheck{ + Type: "RPA", + Category: common.CategoryRobocorpHomeMembers, + Status: statusWarning, + Message: fmt.Sprintf("More than one user is sharing ROBOCORP_HOME (%s). Those users are: %s.", common.RobocorpHome(), members), + Link: supportGeneralUrl, + } +} + func robocorpHomeCheck() *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") if !conda.ValidLocation(common.RobocorpHome()) { From ce5a075baa5465ebdba17a968660077f4ed70eff Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 7 Jun 2023 09:29:54 +0300 Subject: [PATCH 402/516] Micromamba upgrade (v14.9.1) - micromamba upgrade to v1.4.3 --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 39477df0..7eb0fcd2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.9.0` + Version = `v14.9.1` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index be94c66e..e803d1d2 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_002 - MicromambaVersionNumber = "v1.4.2" + MicromambaVersionLimit = 1_004_003 + MicromambaVersionNumber = "v1.4.3" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index 79e57ff8..edf8367b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v14.9.1 (date: 7.6.2023) + +- micromamba upgrade to v1.4.3 + ## v14.9.0 (date: 7.6.2023) - added one user per `ROBOCORP_HOME` warnings From e8f3a01b14f7c669c6e401bc5930b86655dd4954 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 8 Jun 2023 12:07:02 +0300 Subject: [PATCH 403/516] Code cleanup and artifactsDir content (v14.9.2) - more cleaning up of dead code and data structures - made it visible if artifactsDir already have files before run starts --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/cache.go | 8 -------- operations/running.go | 5 ++++- pathlib/validators.go | 25 +++++++++++++++++++++++-- pretty/functions.go | 2 +- settings/data.go | 33 --------------------------------- 7 files changed, 34 insertions(+), 46 deletions(-) diff --git a/common/version.go b/common/version.go index 7eb0fcd2..7cccfdaf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.9.1` + Version = `v14.9.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index edf8367b..8a5fa389 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.9.2 (date: 8.6.2023) + +- more cleaning up of dead code and data structures +- made it visible if artifactsDir already have files before run starts + ## v14.9.1 (date: 7.6.2023) - micromamba upgrade to v1.4.3 diff --git a/operations/cache.go b/operations/cache.go index a76028fa..d072f648 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -12,13 +12,6 @@ import ( "gopkg.in/yaml.v2" ) -type Folder struct { - Path string `yaml:"path" json:"robot"` - Created int64 `yaml:"created" json:"created"` - Updated int64 `yaml:"updated" json:"updated"` - Deleted int64 `yaml:"deleted" json:"deleted"` -} - type Credential struct { Account string `yaml:"account"` Context string `yaml:"context"` @@ -26,7 +19,6 @@ type Credential struct { Deadline int64 `yaml:"deadline"` } -type FolderMap map[string]*Folder type CredentialMap map[string]*Credential type StampMap map[string]int64 diff --git a/operations/running.go b/operations/running.go index 122aa189..108dbacb 100644 --- a/operations/running.go +++ b/operations/running.go @@ -184,6 +184,7 @@ func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, co defer common.TimelineEnd() pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) if simple { + pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory()) ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { ExecuteTask(runFlags, template, config, todo, label, interactive, extraEnv) @@ -305,8 +306,10 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro wantedfile, _ := config.DependenciesFile() ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } - FreezeEnvironmentListing(label, config) + pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory()) + + FreezeEnvironmentListing(label, config) preRunScripts := config.PreRunScripts() if !common.DeveloperFlag && preRunScripts != nil && len(preRunScripts) > 0 { common.Timeline("pre run scripts started") diff --git a/pathlib/validators.go b/pathlib/validators.go index 03bc4831..ee39edc9 100644 --- a/pathlib/validators.go +++ b/pathlib/validators.go @@ -3,6 +3,8 @@ package pathlib import ( "fmt" "os" + + "github.com/robocorp/rcc/pretty" ) func FileExist(name string) bool { @@ -20,13 +22,32 @@ func EnsureDirectoryExists(directory string) error { func EnsureEmptyDirectory(directory string) error { fullpath, err := EnsureDirectory(directory) - handle, err := os.Open(fullpath) if err != nil { return err } - entries, err := handle.Readdir(-1) + entries, err := os.ReadDir(fullpath) + if err != nil { + return err + } if len(entries) > 0 { return fmt.Errorf("Directory %s is not empty!", fullpath) } return nil } + +func NoteDirectoryContent(context, directory string) { + if !IsDir(directory) { + return + } + fullpath, err := Abs(directory) + if err != nil { + return + } + entries, err := os.ReadDir(fullpath) + if err != nil { + return + } + for _, entry := range entries { + pretty.Note("%s %q already has %q in it.", context, fullpath, entry.Name()) + } +} diff --git a/pretty/functions.go b/pretty/functions.go index 53f29f0f..4d37d65b 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -27,7 +27,7 @@ func Highlight(format string, rest ...interface{}) { } func Lowlight(format string, rest ...interface{}) { - niceform := fmt.Sprintf("%s%s%s%s", Grey, Faint, format, Reset) + niceform := fmt.Sprintf("%s%s%s", Grey, format, Reset) common.Log(niceform, rest...) } diff --git a/settings/data.go b/settings/data.go index b2180c66..0d65686a 100644 --- a/settings/data.go +++ b/settings/data.go @@ -247,19 +247,6 @@ func (it *Certificates) onTopOf(target *Settings) { } } -type Endpoints struct { - CloudApi string `yaml:"cloud-api,omitempty" json:"cloud-api,omitempty"` - CloudLinking string `yaml:"cloud-linking,omitempty" json:"cloud-linking,omitempty"` - CloudUi string `yaml:"cloud-ui,omitempty" json:"cloud-ui,omitempty"` - Conda string `yaml:"conda,omitempty" json:"conda,omitempty"` - Docs string `yaml:"docs,omitempty" json:"docs,omitempty"` - Downloads string `yaml:"downloads,omitempty" json:"downloads,omitempty"` - Issues string `yaml:"issues,omitempty" json:"issues,omitempty"` - Pypi string `yaml:"pypi,omitempty" json:"pypi,omitempty"` - PypiTrusted string `yaml:"pypi-trusted,omitempty" json:"pypi-trusted,omitempty"` - Telemetry string `yaml:"telemetry,omitempty" json:"telemetry,omitempty"` -} - func justHostAndPort(link string) string { if len(link) == 0 { return "" @@ -279,26 +266,6 @@ func hostFromUrl(link string, collector map[string]bool) { } } -func (it *Endpoints) Hostnames() []string { - collector := make(map[string]bool) - hostFromUrl(it.CloudApi, collector) - hostFromUrl(it.CloudLinking, collector) - hostFromUrl(it.CloudUi, collector) - hostFromUrl(it.Conda, collector) - hostFromUrl(it.Docs, collector) - hostFromUrl(it.Downloads, collector) - hostFromUrl(it.Issues, collector) - hostFromUrl(it.Pypi, collector) - hostFromUrl(it.PypiTrusted, collector) - hostFromUrl(it.Telemetry, collector) - result := make([]string, 0, len(collector)) - for key, _ := range collector { - result = append(result, key) - } - sort.Strings(result) - return result -} - type Meta struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` From 9d546aaaf5cbd22c479890eefae603575cd7e93e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 12 Jun 2023 14:57:09 +0300 Subject: [PATCH 404/516] Info file creation and signal handling (v14.10.0) - saving separate info file for catalogs and holotrees (to speed up some commands in future) - added interrupt signal ignoring around robot run, so that robot can actually react and respond to interrupt (and if send twice, then second interrupt will actually interrupt rcc) --- common/version.go | 2 +- docs/changelog.md | 8 +++ htfs/directory.go | 120 +++++++++++++++++++++++++++--------------- htfs/library.go | 4 +- operations/running.go | 12 +++-- shell/task.go | 45 +++++++++++----- 6 files changed, 130 insertions(+), 61 deletions(-) diff --git a/common/version.go b/common/version.go index 7cccfdaf..94b5c4d8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.9.2` + Version = `v14.10.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 8a5fa389..43eb3b18 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v14.10.0 (date: 12.6.2023) + +- saving separate info file for catalogs and holotrees (to speed up some + commands in future) +- added interrupt signal ignoring around robot run, so that robot can actually + react and respond to interrupt (and if send twice, then second interrupt + will actually interrupt rcc) + ## v14.9.2 (date: 8.6.2023) - more cleaning up of dead code and data structures diff --git a/htfs/directory.go b/htfs/directory.go index e1b0735e..03d76cf8 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -39,24 +39,69 @@ func init() { pathlib.MakeSharedDir(common.HololibPids()) } -type Filetask func(string, *File) anywork.Work -type Dirtask func(string, *Dir) anywork.Work -type Treetop func(string, *Dir) error +type ( + Filetask func(string, *File) anywork.Work + Dirtask func(string, *Dir) anywork.Work + Treetop func(string, *Dir) error + + Info struct { + RccVersion string `json:"rcc"` + Identity string `json:"identity"` + Path string `json:"path"` + Controller string `json:"controller"` + Space string `json:"space"` + Platform string `json:"platform"` + Blueprint string `json:"blueprint"` + } + + Root struct { + *Info + Lifted bool `json:"lifted"` + Tree *Dir `json:"tree"` + source string + } + + Roots []*Root + Dir struct { + Name string `json:"name"` + Symlink string `json:"symlink,omitempty"` + Mode fs.FileMode `json:"mode"` + Dirs map[string]*Dir `json:"subdirs"` + Files map[string]*File `json:"files"` + Shadow bool `json:"shadow,omitempty"` + } + + File struct { + Name string `json:"name"` + Symlink string `json:"symlink,omitempty"` + Size int64 `json:"size"` + Mode fs.FileMode `json:"mode"` + Digest string `json:"digest"` + Rewrite []int64 `json:"rewrite"` + } +) -type Root struct { - RccVersion string `json:"rcc"` - Identity string `json:"identity"` - Path string `json:"path"` - Controller string `json:"controller"` - Space string `json:"space"` - Platform string `json:"platform"` - Blueprint string `json:"blueprint"` - Lifted bool `json:"lifted"` - Tree *Dir `json:"tree"` - source string +func (it *Info) AsJson() ([]byte, error) { + return json.MarshalIndent(it, "", " ") } -type Roots []*Root +func (it *Info) saveAs(filename string) error { + content, err := it.AsJson() + if err != nil { + return err + } + sink, err := pathlib.Create(filename) + if err != nil { + return err + } + defer sink.Close() + defer sink.Sync() + _, err = sink.Write(content) + if err != nil { + return err + } + return nil +} func (it Roots) BaseFolders() []string { result := []string{} @@ -132,20 +177,29 @@ func (it Roots) RemoveHolotreeSpace(label string) (err error) { return nil } -func NewRoot(path string) (*Root, error) { +func NewInfo(path string) (*Info, error) { fullpath, err := pathlib.Abs(path) if err != nil { return nil, err } - basename := filepath.Base(fullpath) - return &Root{ - Identity: basename, + return &Info{ + RccVersion: common.Version, + Identity: filepath.Base(fullpath), Path: fullpath, Platform: common.Platform(), - Lifted: false, - Tree: newDir("", "", false), - RccVersion: common.Version, - source: fullpath, + }, nil +} + +func NewRoot(path string) (*Root, error) { + info, err := NewInfo(path) + if err != nil { + return nil, err + } + return &Root{ + Info: info, + Lifted: false, + Tree: newDir("", "", false), + source: info.Path, }, nil } @@ -273,7 +327,7 @@ func (it *Root) SaveAs(filename string) error { if err != nil { return err } - return nil + return it.Info.saveAs(filename + ".info") } func (it *Root) ReadFrom(source io.Reader) error { @@ -297,15 +351,6 @@ func (it *Root) LoadFrom(filename string) error { return it.ReadFrom(reader) } -type Dir struct { - Name string `json:"name"` - Symlink string `json:"symlink,omitempty"` - Mode fs.FileMode `json:"mode"` - Dirs map[string]*Dir `json:"subdirs"` - Files map[string]*File `json:"files"` - Shadow bool `json:"shadow,omitempty"` -} - func showFile(filename string) (content []byte, err error) { defer fail.Around(&err) @@ -405,15 +450,6 @@ func (it *Dir) Lift(path string) error { return nil } -type File struct { - Name string `json:"name"` - Symlink string `json:"symlink,omitempty"` - Size int64 `json:"size"` - Mode fs.FileMode `json:"mode"` - Digest string `json:"digest"` - Rewrite []int64 `json:"rewrite"` -} - func (it *File) IsSymlink() bool { return len(it.Symlink) > 0 } diff --git a/htfs/library.go b/htfs/library.go index 8a316b2d..b7d7a49d 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -307,7 +307,9 @@ func (it *hololib) queryBlueprint(key string) bool { func CatalogNames() []string { result := make([]string, 0, 10) for _, catalog := range pathlib.Glob(common.HololibCatalogLocation(), "[0-9a-f]*v12.*") { - result = append(result, filepath.Base(catalog)) + if filepath.Ext(catalog) != ".info" { + result = append(result, filepath.Base(catalog)) + } } return set.Set(result) } diff --git a/operations/running.go b/operations/running.go index 108dbacb..3165d619 100644 --- a/operations/running.go +++ b/operations/running.go @@ -335,11 +335,13 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro common.Debug("about to run command - %v", task) journal.CurrentBuildEvent().RobotStarts() - if common.NoOutputCapture { - _, err = shell.New(environment, directory, task...).Execute(interactive) - } else { - _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) - } + shell.WithInterrupt(func() { + if common.NoOutputCapture { + _, err = shell.New(environment, directory, task...).Execute(interactive) + } else { + _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + } + }) journal.CurrentBuildEvent().RobotEnds() after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) diff --git a/shell/task.go b/shell/task.go index 883e8dc9..6afc6413 100644 --- a/shell/task.go +++ b/shell/task.go @@ -5,26 +5,32 @@ import ( "io" "os" "os/exec" + "os/signal" "path/filepath" "github.com/google/shlex" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) -type Common interface { - Debug(string, ...interface{}) error - Trace(string, ...interface{}) error - Timeline(string, ...interface{}) -} +type ( + Common interface { + Debug(string, ...interface{}) error + Trace(string, ...interface{}) error + Timeline(string, ...interface{}) + } -type Task struct { - environment []string - directory string - executable string - args []string - stderronly bool -} + Task struct { + environment []string + directory string + executable string + args []string + stderronly bool + } + + Wrapper func() +) func Split(commandline string) ([]string, error) { return shlex.Split(commandline) @@ -145,3 +151,18 @@ func (it *Task) CaptureOutput() (string, int, error) { code, err := it.execute(stdin, stdout, os.Stderr) return stdout.String(), code, err } + +func WithInterrupt(task Wrapper) { + signals := make(chan os.Signal, 1) + defer signal.Stop(signals) + defer close(signals) + go func() { + signal.Notify(signals, os.Interrupt) + got, ok := <-signals + if ok { + pretty.Note("Detected and ignored %q signal. Second one will not be ignored. [rcc]", got) + } + signal.Stop(signals) + }() + task() +} From 7f396b2e995733707e8312a11ea29462fdbab239 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 12 Jun 2023 15:49:20 +0300 Subject: [PATCH 405/516] Immediate switch to imported profile (v14.11.0) - added `--switch` option to profile import to immediately switch to imported profile once it is successfully imported --- cmd/configureimport.go | 8 ++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ robot_tests/profile_gamma.yaml | 14 ++++++++++++++ robot_tests/profiles.robot | 11 +++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 robot_tests/profile_gamma.yaml diff --git a/cmd/configureimport.go b/cmd/configureimport.go index 76ed3723..e480c061 100644 --- a/cmd/configureimport.go +++ b/cmd/configureimport.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/cobra" ) +var ( + immediateSwitch bool +) + var configureImportCmd = &cobra.Command{ Use: "import", Short: "Import a configuration profile for Robocorp tooling.", @@ -21,12 +25,16 @@ var configureImportCmd = &cobra.Command{ pretty.Guard(err == nil, 1, "Error while loading profile: %v", err) err = profile.Import() pretty.Guard(err == nil, 2, "Error while importing profile: %v", err) + if immediateSwitch { + switchProfileTo(profile.Name) + } pretty.Ok() }, } func init() { configureCmd.AddCommand(configureImportCmd) + configureImportCmd.Flags().BoolVarP(&immediateSwitch, "switch", "s", false, "Immediately switch to use new profile.") configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename to import as configuration profile.") configureImportCmd.MarkFlagRequired("filename") } diff --git a/common/version.go b/common/version.go index 94b5c4d8..5db1e859 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.10.0` + Version = `v14.11.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 43eb3b18..15f9fd7d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.11.0 (date: 12.6.2023) + +- added `--switch` option to profile import to immediately switch to imported + profile once it is successfully imported + ## v14.10.0 (date: 12.6.2023) - saving separate info file for catalogs and holotrees (to speed up some diff --git a/robot_tests/profile_gamma.yaml b/robot_tests/profile_gamma.yaml new file mode 100644 index 00000000..3eb32489 --- /dev/null +++ b/robot_tests/profile_gamma.yaml @@ -0,0 +1,14 @@ +name: Gamma +description: Gamma settings +settings: + certificates: + verify-ssl: true + ssl-no-revoke: false + network: + https-proxy: "" + http-proxy: "" + meta: + name: Gamma + description: Gamma settings + source: tests + version: 1.2.3.4 diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index ea04d083..e1d22675 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -79,6 +79,17 @@ Goal: Diagnostics can show beta profile information Must Have "config-https-proxy": "http://bad.betaputkinen.net:1234/" Must Have "config-http-proxy": "http://bad.betaputkinen.net:2345/" +Goal: Can import and switch to Gamma profile immediately + Step build/rcc configuration import --filename robot_tests/profile_gamma.yaml --switch + Use STDERR + Must Have OK. + + Step build/rcc configuration switch + Use STDOUT + Must Have Currently active profile is: Gamma + Use STDERR + Must Have OK. + Goal: Can switch to no profile Step build/rcc configuration switch --noprofile Use STDERR From fe03321b0f7531e2e7064f6a96723e00bd101ffd Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 13 Jun 2023 15:53:40 +0300 Subject: [PATCH 406/516] Subprocess managing (v14.12.0) - adding listing of still running processes after robot run - upgrading github actions to use go v1.20.x - bugfix: panic when using lockpids with nil value --- .github/workflows/rcc.yaml | 4 +- common/version.go | 2 +- docs/changelog.md | 6 +++ go.mod | 1 + go.sum | 2 + operations/processtree.go | 95 ++++++++++++++++++++++++++++++++++++++ operations/running.go | 4 ++ pathlib/lockpids.go | 5 +- shell/task.go | 2 + 9 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 operations/processtree.go diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index 5d04077f..f00d615b 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.19.x' + go-version: '1.20.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.19.x' + go-version: '1.20.x' - uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' diff --git a/common/version.go b/common/version.go index 5db1e859..30cef114 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.11.0` + Version = `v14.12.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 15f9fd7d..657132d3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v14.12.0 (date: 13.6.2023) + +- adding listing of still running processes after robot run +- upgrading github actions to use go v1.20.x +- bugfix: panic when using lockpids with nil value + ## v14.11.0 (date: 12.6.2023) - added `--switch` option to profile import to immediately switch to imported diff --git a/go.mod b/go.mod index da896b64..f9c7a983 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect diff --git a/go.sum b/go.sum index ff1044d0..26aa0e61 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,8 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= diff --git a/operations/processtree.go b/operations/processtree.go new file mode 100644 index 00000000..5a58ecd3 --- /dev/null +++ b/operations/processtree.go @@ -0,0 +1,95 @@ +package operations + +import ( + "fmt" + "os" + "sort" + + "github.com/mitchellh/go-ps" + "github.com/robocorp/rcc/pretty" +) + +type ( + ProcessMap map[int]*ProcessNode + ProcessNode struct { + Pid int + Parent int + Executable string + Children ProcessMap + } +) + +func NewProcessNode(core ps.Process) *ProcessNode { + return &ProcessNode{ + Pid: core.Pid(), + Parent: core.PPid(), + Executable: core.Executable(), + Children: make(ProcessMap), + } +} + +func ProcessMapNow() (ProcessMap, error) { + processes, err := ps.Processes() + if err != nil { + return nil, err + } + result := make(ProcessMap) + for _, process := range processes { + result[process.Pid()] = NewProcessNode(process) + } + for pid, process := range result { + parent, ok := result[process.Parent] + if ok { + parent.Children[pid] = process + } + } + return result, nil +} + +func (it ProcessMap) Keys() []int { + keys := make([]int, 0, len(it)) + for key, _ := range it { + keys = append(keys, key) + } + sort.Ints(keys) + return keys +} + +func (it *ProcessNode) warnings() { + pretty.Warning("%q process %d still has running subprocesses:", it.Executable, it.Pid) + it.warningTree("> ") + pretty.Note("Depending on OS, above processes may prevent robot to close properly.") + pretty.Note("Few reasons why this might be happening are:") + pretty.Note("- robot is not properly releasing all resources that it is using") + pretty.Note("- there was failure inside robot, which caused robot to exit without proper cleanup") + pretty.Note("- developer intentionally left processes running, which is not good for repeatable automation") + pretty.Highlight("So if you see this message, and robot still seems to be running, it is not!") + pretty.Highlight("You now have to take action and stop those processes that are preventing robot to complete.") +} + +func (it *ProcessNode) warningTree(prefix string) { + kind := "leaf" + if len(it.Children) > 0 { + kind = "container" + } + pretty.Warning("%s%d %q <%s>", prefix, it.Pid, it.Executable, kind) + indent := prefix + "| " + for _, key := range it.Children.Keys() { + it.Children[key].warningTree(indent) + } +} + +func SubprocessWarning() error { + processes, err := ProcessMapNow() + if err != nil { + return err + } + self, ok := processes[os.Getpid()] + if !ok { + return fmt.Errorf("For some reason, could not find own process in process map.") + } + if len(self.Children) > 0 { + self.warnings() + } + return nil +} diff --git a/operations/running.go b/operations/running.go index 3165d619..22477f4c 100644 --- a/operations/running.go +++ b/operations/running.go @@ -342,6 +342,10 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } }) + err = SubprocessWarning() + if err != nil { + pretty.Warning("Problem with subprocess warnings, reason: %v", err) + } journal.CurrentBuildEvent().RobotEnds() after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) diff --git a/pathlib/lockpids.go b/pathlib/lockpids.go index 03730347..34d48e8c 100644 --- a/pathlib/lockpids.go +++ b/pathlib/lockpids.go @@ -74,6 +74,9 @@ browsing: for _, entry := range entries { fullpath := filepath.Join(root, entry.Name()) info, err := entry.Info() + if err != nil || info == nil { + continue + } if info.IsDir() { anywork.Backlog(func() { TryRemoveAll("lockpid/dir", fullpath) @@ -81,7 +84,7 @@ browsing: }) continue browsing } - if err == nil && info.ModTime().Before(deadline) { + if info.ModTime().Before(deadline) { anywork.Backlog(func() { TryRemove("lockpid/stale", fullpath) common.Trace(">> Trying to remove old file at lockpids: %q", fullpath) diff --git a/shell/task.go b/shell/task.go index 6afc6413..d92e1831 100644 --- a/shell/task.go +++ b/shell/task.go @@ -7,6 +7,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "time" "github.com/google/shlex" "github.com/robocorp/rcc/common" @@ -67,6 +68,7 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) command.Stdin = stdin command.Stdout = stdout command.Stderr = stderr + command.WaitDelay = 2 * time.Minute err := command.Start() if err != nil { return -500, err From a5e07b5d9576a2f2f0746c26e1b4f7865e5cf2ac Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 15 Jun 2023 08:48:18 +0300 Subject: [PATCH 407/516] Subprocess managing improved (v14.13.0) - improved listing of still running processes - set process wait delay to 15 seconds after process has completed but has not released it IO pipes yet --- common/version.go | 2 +- docs/changelog.md | 6 +++ operations/processtree.go | 105 ++++++++++++++++++++++++++++++++++---- operations/running.go | 5 +- shell/task.go | 2 +- 5 files changed, 108 insertions(+), 12 deletions(-) diff --git a/common/version.go b/common/version.go index 30cef114..0b293dcc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.12.0` + Version = `v14.13.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 657132d3..bb59b325 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v14.13.0 (date: 15.6.2023) + +- improved listing of still running processes +- set process wait delay to 15 seconds after process has completed but has not + released it IO pipes yet + ## v14.12.0 (date: 13.6.2023) - adding listing of still running processes after robot run diff --git a/operations/processtree.go b/operations/processtree.go index 5a58ecd3..d00533ee 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -4,12 +4,14 @@ import ( "fmt" "os" "sort" + "time" "github.com/mitchellh/go-ps" "github.com/robocorp/rcc/pretty" ) type ( + ChildMap map[int]string ProcessMap map[int]*ProcessNode ProcessNode struct { Pid int @@ -55,31 +57,45 @@ func (it ProcessMap) Keys() []int { return keys } -func (it *ProcessNode) warnings() { - pretty.Warning("%q process %d still has running subprocesses:", it.Executable, it.Pid) - it.warningTree("> ") +func (it *ProcessNode) warnings(additional ProcessMap) { + if len(it.Children) > 0 { + pretty.Warning("%q process %d still has running subprocesses:", it.Executable, it.Pid) + it.warningTree("> ", false) + } else { + pretty.Warning("%q process %d still has running migrated processes:", it.Executable, it.Pid) + } + if len(additional) > 0 { + pretty.Warning("+ migrated process still running:") + for _, zombie := range additional { + zombie.warningTree("| ", true) + } + } pretty.Note("Depending on OS, above processes may prevent robot to close properly.") pretty.Note("Few reasons why this might be happening are:") pretty.Note("- robot is not properly releasing all resources that it is using") + pretty.Note("- robot is generating background processes that don't complete before robot tries to exit") pretty.Note("- there was failure inside robot, which caused robot to exit without proper cleanup") pretty.Note("- developer intentionally left processes running, which is not good for repeatable automation") pretty.Highlight("So if you see this message, and robot still seems to be running, it is not!") pretty.Highlight("You now have to take action and stop those processes that are preventing robot to complete.") } -func (it *ProcessNode) warningTree(prefix string) { +func (it *ProcessNode) warningTree(prefix string, newparent bool) { kind := "leaf" if len(it.Children) > 0 { kind = "container" } - pretty.Warning("%s%d %q <%s>", prefix, it.Pid, it.Executable, kind) + if newparent { + kind = fmt.Sprintf("%s -> new parent PID: #%d", kind, it.Parent) + } + pretty.Warning("%s#%d %q <%s>", prefix, it.Pid, it.Executable, kind) indent := prefix + "| " for _, key := range it.Children.Keys() { - it.Children[key].warningTree(indent) + it.Children[key].warningTree(indent, false) } } -func SubprocessWarning() error { +func SubprocessWarning(seen ChildMap, use bool) error { processes, err := ProcessMapNow() if err != nil { return err @@ -88,8 +104,79 @@ func SubprocessWarning() error { if !ok { return fmt.Errorf("For some reason, could not find own process in process map.") } - if len(self.Children) > 0 { - self.warnings() + masked := make(ChildMap) + if use { + for pid, executable := range seen { + ref, ok := processes[pid] + if ok { + updateActiveChildren(ref, masked) + } else { + masked[pid] = executable + } + } + } + for key, _ := range masked { + delete(seen, key) + } + additional := make(ProcessMap) + for pid, executable := range seen { + ref, ok := processes[pid] + if ok && executable == ref.Executable { + additional[pid] = ref + } + } + if len(self.Children) > 0 || len(additional) > 0 { + self.warnings(additional) } return nil } + +func removeStaleChildren(processes ProcessMap, seen ChildMap) { + for key, name := range seen { + found, ok := processes[key] + if !ok || found.Executable != name { + delete(seen, key) + } + } +} + +func updateActiveChildren(host *ProcessNode, seen ChildMap) { + for pid, child := range host.Children { + seen[pid] = child.Executable + updateActiveChildren(child, seen) + } +} + +func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) { + source, ok := processes[pid] + if ok { + removeStaleChildren(processes, seen) + updateActiveChildren(source, seen) + } +} + +func WatchChildren(pid int, delay time.Duration) chan ChildMap { + pipe := make(chan ChildMap) + go babySitter(pid, pipe, delay) + return pipe +} + +func babySitter(pid int, reply chan ChildMap, delay time.Duration) { + defer close(reply) + seen := make(ChildMap) + failures := 0 +forever: + for failures < 10 { + processes, err := ProcessMapNow() + if err == nil { + updateSeenChildren(pid, processes, seen) + failures = 0 + } + select { + case reply <- seen: + break forever + case <-time.After(delay): + continue forever + } + } +} diff --git a/operations/running.go b/operations/running.go index 22477f4c..3f0895de 100644 --- a/operations/running.go +++ b/operations/running.go @@ -2,6 +2,7 @@ package operations import ( "fmt" + "os" "path/filepath" "runtime" "strings" @@ -335,6 +336,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro common.Debug("about to run command - %v", task) journal.CurrentBuildEvent().RobotStarts() + pipe := WatchChildren(os.Getpid(), 2*time.Second) shell.WithInterrupt(func() { if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) @@ -342,7 +344,8 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } }) - err = SubprocessWarning() + seen, ok := <-pipe + err = SubprocessWarning(seen, ok) if err != nil { pretty.Warning("Problem with subprocess warnings, reason: %v", err) } diff --git a/shell/task.go b/shell/task.go index d92e1831..b3b65b15 100644 --- a/shell/task.go +++ b/shell/task.go @@ -68,7 +68,7 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) command.Stdin = stdin command.Stdout = stdout command.Stderr = stderr - command.WaitDelay = 2 * time.Minute + command.WaitDelay = 15 * time.Second err := command.Start() if err != nil { return -500, err From 89b541aaa3ef28a3375c74b8775fc1a0854c0e5a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 20 Jun 2023 11:47:28 +0300 Subject: [PATCH 408/516] Minor bug fixes (v14.13.1) - bugfix: fixing exit code masking by subprocess handling - predicting rcc exit code made visible - making robot run exit code more visible - robot tests now use special settings.yaml to prevent template updates and will only use internal templates for testing --- cmd/rcc/main.go | 1 + common/version.go | 2 +- docs/changelog.md | 8 ++++++++ operations/running.go | 8 ++++---- robot_tests/export_holozip.robot | 8 ++++---- robot_tests/resources.robot | 9 +++++++-- robot_tests/settings.yaml | 2 ++ 7 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 robot_tests/settings.yaml diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index d37d1534..ede34bb3 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -77,6 +77,7 @@ func ExitProtection() { exit, ok := status.(common.ExitCode) if ok { exit.ShowMessage() + pretty.Highlight("[rcc] exit status will be: %d!", exit.Code) cloud.WaitTelemetry() common.WaitLogs() os.Exit(exit.Code) diff --git a/common/version.go b/common/version.go index 0b293dcc..05b56824 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.13.0` + Version = `v14.13.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index bb59b325..7cfdf8b4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v14.13.1 (date: 20.6.2023) + +- bugfix: fixing exit code masking by subprocess handling +- predicting rcc exit code made visible +- making robot run exit code more visible +- robot tests now use special settings.yaml to prevent template updates and + will only use internal templates for testing + ## v14.13.0 (date: 15.6.2023) - improved listing of still running processes diff --git a/operations/running.go b/operations/running.go index 3f0895de..1b76cd70 100644 --- a/operations/running.go +++ b/operations/running.go @@ -345,16 +345,16 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } }) seen, ok := <-pipe - err = SubprocessWarning(seen, ok) - if err != nil { - pretty.Warning("Problem with subprocess warnings, reason: %v", err) + suberr := SubprocessWarning(seen, ok) + if suberr != nil { + pretty.Warning("Problem with subprocess warnings, reason: %v", suberr) } journal.CurrentBuildEvent().RobotEnds() after := make(map[string]string) afterHash, afterErr := conda.DigestFor(label, after) conda.DiagnoseDirty(label, label, beforeHash, afterHash, beforeErr, afterErr, before, after, true) if err != nil { - pretty.Exit(10, "Error: %v", err) + pretty.Exit(10, "Error: %v (robot run exit)", err) } pretty.Ok() } diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index b4c627bf..28b1bdb7 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -10,11 +10,11 @@ Export setup Remove Directory tmp/developer True Remove Directory tmp/guest True Remove Directory tmp/standalone True - Set Environment Variable ROBOCORP_HOME tmp/developer + Prepare Robocorp Home tmp/developer Fire And Forget build/rcc ht delete 4e67cd8 Export teardown - Set Environment Variable ROBOCORP_HOME tmp/robocorp + Prepare Robocorp Home tmp/robocorp Remove Directory tmp/developer True Remove Directory tmp/guest True Remove Directory tmp/standalone True @@ -83,13 +83,13 @@ Goal: Can delete author space Goal: Can run as guest Fire And Forget build/rcc ht delete 4e67cd8 - Set Environment Variable ROBOCORP_HOME tmp/guest + Prepare Robocorp Home tmp/guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR Must Have OK. Goal: Space created under author for guest - Set Environment Variable ROBOCORP_HOME tmp/developer + Prepare Robocorp Home tmp/developer Step build/rcc ht ls Use STDERR Wont Have 4e67cd8_fcb4b859 diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index dd5ac8cb..2f55530a 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -7,13 +7,18 @@ Library supporting.py Clean Local Remove Directory tmp/robocorp True +Prepare Robocorp Home + [Arguments] ${location} + Create Directory ${location} + Set Environment Variable ROBOCORP_HOME ${location} + Copy File robot_tests/settings.yaml ${location}/settings.yaml + Prepare Local Remove Directory tmp/fluffy True Remove Directory tmp/nodogs True Remove Directory tmp/robocorp True Remove File tmp/nodogs.zip - Create Directory tmp/robocorp - Set Environment Variable ROBOCORP_HOME tmp/robocorp + Prepare Robocorp Home tmp/robocorp Comment Make sure that tests do not use shared holotree Fire And Forget build/rcc ht init --revoke diff --git a/robot_tests/settings.yaml b/robot_tests/settings.yaml new file mode 100644 index 00000000..ac2b3098 --- /dev/null +++ b/robot_tests/settings.yaml @@ -0,0 +1,2 @@ +autoupdates: + templates: https://downloads.robocorp.com/templates/not_available.yaml From ba1e9417b9561bfe416f4d5a5cd6e441634157b8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 21 Jun 2023 11:49:25 +0300 Subject: [PATCH 409/516] Micromamba downgrade due bug (v14.13.2) - micromamba downgrade to v1.4.2, because micromamba bug in Windows --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 18 +++++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 05b56824..3ec0f8d9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.13.1` + Version = `v14.13.2` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index e803d1d2..be94c66e 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_003 - MicromambaVersionNumber = "v1.4.3" + MicromambaVersionLimit = 1_004_002 + MicromambaVersionNumber = "v1.4.2" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index 7cfdf8b4..a031ed95 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,10 @@ # rcc change log -## v14.13.1 (date: 20.6.2023) +## v14.13.2 (date: 21.6.2023) + +- micromamba downgrade to v1.4.2, because micromamba bug in Windows + +## v14.13.1 (date: 20.6.2023) UNSTABLE - bugfix: fixing exit code masking by subprocess handling - predicting rcc exit code made visible @@ -8,24 +12,24 @@ - robot tests now use special settings.yaml to prevent template updates and will only use internal templates for testing -## v14.13.0 (date: 15.6.2023) +## v14.13.0 (date: 15.6.2023) UNSTABLE - improved listing of still running processes - set process wait delay to 15 seconds after process has completed but has not released it IO pipes yet -## v14.12.0 (date: 13.6.2023) +## v14.12.0 (date: 13.6.2023) UNSTABLE - adding listing of still running processes after robot run - upgrading github actions to use go v1.20.x - bugfix: panic when using lockpids with nil value -## v14.11.0 (date: 12.6.2023) +## v14.11.0 (date: 12.6.2023) UNSTABLE - added `--switch` option to profile import to immediately switch to imported profile once it is successfully imported -## v14.10.0 (date: 12.6.2023) +## v14.10.0 (date: 12.6.2023) UNSTABLE - saving separate info file for catalogs and holotrees (to speed up some commands in future) @@ -33,12 +37,12 @@ react and respond to interrupt (and if send twice, then second interrupt will actually interrupt rcc) -## v14.9.2 (date: 8.6.2023) +## v14.9.2 (date: 8.6.2023) UNSTABLE - more cleaning up of dead code and data structures - made it visible if artifactsDir already have files before run starts -## v14.9.1 (date: 7.6.2023) +## v14.9.1 (date: 7.6.2023) UNSTABLE - micromamba upgrade to v1.4.3 From 870ac6a57d5b7664a6b8ff53c07329c8a4c55e1f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 22 Jun 2023 09:28:16 +0300 Subject: [PATCH 410/516] Minor improvements (v14.13.3) - faster heartbeat for snapshotting subprocesses during robot run (200ms) - added guiding text on "non-empty artifacts directory case" --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 6 +++--- pathlib/validators.go | 8 +++++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 3ec0f8d9..d7aa0e43 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.13.2` + Version = `v14.13.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index a031ed95..e7f7dfb5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.13.3 (date: 22.6.2023) + +- faster heartbeat for snapshotting subprocesses during robot run (200ms) +- added guiding text on "non-empty artifacts directory case" + ## v14.13.2 (date: 21.6.2023) - micromamba downgrade to v1.4.2, because micromamba bug in Windows diff --git a/operations/running.go b/operations/running.go index 1b76cd70..bb3dcbee 100644 --- a/operations/running.go +++ b/operations/running.go @@ -185,7 +185,7 @@ func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, co defer common.TimelineEnd() pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) if simple { - pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory()) + pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory(), true) ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { ExecuteTask(runFlags, template, config, todo, label, interactive, extraEnv) @@ -308,7 +308,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } - pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory()) + pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory(), true) FreezeEnvironmentListing(label, config) preRunScripts := config.PreRunScripts() @@ -336,7 +336,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro common.Debug("about to run command - %v", task) journal.CurrentBuildEvent().RobotStarts() - pipe := WatchChildren(os.Getpid(), 2*time.Second) + pipe := WatchChildren(os.Getpid(), 200*time.Millisecond) shell.WithInterrupt(func() { if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) diff --git a/pathlib/validators.go b/pathlib/validators.go index ee39edc9..0c58dc62 100644 --- a/pathlib/validators.go +++ b/pathlib/validators.go @@ -35,7 +35,7 @@ func EnsureEmptyDirectory(directory string) error { return nil } -func NoteDirectoryContent(context, directory string) { +func NoteDirectoryContent(context, directory string, guide bool) { if !IsDir(directory) { return } @@ -50,4 +50,10 @@ func NoteDirectoryContent(context, directory string) { for _, entry := range entries { pretty.Note("%s %q already has %q in it.", context, fullpath, entry.Name()) } + if guide && len(entries) > 0 { + pretty.Highlight("Above notes mean, that there were files present in directory that was supposed to be empty!") + pretty.Highlight("In robot development phase, it might be ok to have these files while building robot.") + pretty.Highlight("In production robot/assistant, this might be mistake, where development files were") + pretty.Highlight("left inside robot.zip file. Report these to developer who made this robot/assistant.") + } } From 101afce0d3a7c722e1589c3c63b6efed9faced17 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 27 Jun 2023 08:17:40 +0300 Subject: [PATCH 411/516] Highlighting robot run status (v14.14.0) - unless silenced, always show "rcc point of view" of success or failure of actual main robot run, on point of robot process exit --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 18 ++++++++++++++++++ pathlib/validators.go | 2 +- robot_tests/export_holozip.robot | 1 + robot_tests/fullrun.robot | 2 ++ robot_tests/templates.robot | 4 ++++ 7 files changed, 32 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index d7aa0e43..42b7cb82 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.13.3` + Version = `v14.14.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index e7f7dfb5..fc4abbe5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.14.0 (date: 27.6.2023) + +- unless silenced, always show "rcc point of view" of success or failure of + actual main robot run, on point of robot process exit + ## v14.13.3 (date: 22.6.2023) - faster heartbeat for snapshotting subprocesses during robot run (200ms) diff --git a/operations/running.go b/operations/running.go index bb3dcbee..a7d8fed5 100644 --- a/operations/running.go +++ b/operations/running.go @@ -18,6 +18,10 @@ import ( "github.com/robocorp/rcc/shell" ) +const ( + rccpov = `From rcc point of view, actual main robot run was` +) + var ( rcHosts = []string{"RC_API_SECRET_HOST", "RC_API_WORKITEM_HOST"} rcTokens = []string{"RC_API_SECRET_TOKEN", "RC_API_WORKITEM_TOKEN"} @@ -344,6 +348,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } }) + showRccPointOfView(err) seen, ok := <-pipe suberr := SubprocessWarning(seen, ok) if suberr != nil { @@ -358,3 +363,16 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } pretty.Ok() } + +func showRccPointOfView(err error) { + printer := pretty.Lowlight + message := fmt.Sprintf("@@@ %s SUCCESS. @@@", rccpov) + if err != nil { + printer = pretty.Highlight + message = fmt.Sprintf("@@@ %s FAILURE, reason: %q. See details above. @@@", rccpov, err) + } + banner := strings.Repeat("@", len(message)) + printer(banner) + printer(message) + printer(banner) +} diff --git a/pathlib/validators.go b/pathlib/validators.go index 0c58dc62..5ff64730 100644 --- a/pathlib/validators.go +++ b/pathlib/validators.go @@ -53,7 +53,7 @@ func NoteDirectoryContent(context, directory string, guide bool) { if guide && len(entries) > 0 { pretty.Highlight("Above notes mean, that there were files present in directory that was supposed to be empty!") pretty.Highlight("In robot development phase, it might be ok to have these files while building robot.") - pretty.Highlight("In production robot/assistant, this might be mistake, where development files were") + pretty.Highlight("In production robot/assistant, this might be a mistake, where development files were") pretty.Highlight("left inside robot.zip file. Report these to developer who made this robot/assistant.") } } diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 28b1bdb7..d2ad0bcb 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -86,6 +86,7 @@ Goal: Can run as guest Prepare Robocorp Home tmp/guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Space created under author for guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 297814ba..85275498 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -96,6 +96,7 @@ Goal: Run task in place in debug mode and with timeline. Must Have Version Must Have Origin Must Have Status + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Must Exist tmp/fluffy/output/environment_*_freeze.yaml Must Exist %{ROBOCORP_HOME}/wheels/ @@ -114,6 +115,7 @@ Goal: Run task in clean temporary directory. Wont Have Progress: 09/15 Must Have Progress: 14/15 Must Have Progress: 15/15 + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index 09c06c33..07327801 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -17,6 +17,7 @@ Goal: Standard robot has correct hash. Goal: Running standard robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/standardi/robot.yaml Use STDERR + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Initialize new python robot. @@ -31,6 +32,7 @@ Goal: Python robot has correct hash. Goal: Running python robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/pythoni/robot.yaml Use STDERR + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Initialize new extended robot. @@ -45,11 +47,13 @@ Goal: Extended robot has correct hash. Goal: Running extended robot is succesful. (Run All Tasks) Step build/rcc task run --space templates --task "Run All Tasks" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Running extended robot is succesful. (Run Example Task) Step build/rcc task run --space templates --task "Run Example Task" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR + Must Have From rcc point of view, actual main robot run was SUCCESS. Must Have OK. Goal: Correct holotree spaces were created. From 1a7ba7a8e250bd3d3a7f433a7cc18b5dc1bbfa0f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 3 Aug 2023 09:09:52 +0300 Subject: [PATCH 412/516] Micromamba upgrade v1.4.9 (v14.15.0) - micromamba upgrade to v1.4.9 --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 42b7cb82..d4f0658a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.14.0` + Version = `v14.15.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index be94c66e..e35c25af 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_002 - MicromambaVersionNumber = "v1.4.2" + MicromambaVersionLimit = 1_004_009 + MicromambaVersionNumber = "v1.4.9" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index fc4abbe5..f0472601 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v14.15.0 (date: 3.8.2023) + +- micromamba upgrade to v1.4.9 + ## v14.14.0 (date: 27.6.2023) - unless silenced, always show "rcc point of view" of success or failure of From 7b9823d50b1260b10fa434971bfe19ace4d9c1da Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 9 Aug 2023 14:51:45 +0300 Subject: [PATCH 413/516] Bugfix: stack overflow on process tree (v14.15.1) - bugfix on process tree ending up eating too much stack (stack overflow) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/processtree.go | 16 +++++++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index d4f0658a..2876c022 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.15.0` + Version = `v14.15.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index f0472601..5305ed4d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v14.15.1 (date: 9.8.2023) + +- bugfix on process tree ending up eating too much stack (stack overflow) + ## v14.15.0 (date: 3.8.2023) - micromamba upgrade to v1.4.9 diff --git a/operations/processtree.go b/operations/processtree.go index d00533ee..411b0c7b 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -109,7 +109,7 @@ func SubprocessWarning(seen ChildMap, use bool) error { for pid, executable := range seen { ref, ok := processes[pid] if ok { - updateActiveChildren(ref, masked) + updateActiveChildren(ref, masked, 30) } else { masked[pid] = executable } @@ -140,10 +140,16 @@ func removeStaleChildren(processes ProcessMap, seen ChildMap) { } } -func updateActiveChildren(host *ProcessNode, seen ChildMap) { +func updateActiveChildren(host *ProcessNode, seen ChildMap, maxDepth int) { + if maxDepth < 0 { + return + } for pid, child := range host.Children { - seen[pid] = child.Executable - updateActiveChildren(child, seen) + _, ok := seen[pid] + if !ok { + seen[pid] = child.Executable + updateActiveChildren(child, seen, maxDepth-1) + } } } @@ -151,7 +157,7 @@ func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) { source, ok := processes[pid] if ok { removeStaleChildren(processes, seen) - updateActiveChildren(source, seen) + updateActiveChildren(source, seen, 30) } } From 918604ec4319cb54b7552f12eb1e15533e9ce9e7 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Aug 2023 11:19:56 +0300 Subject: [PATCH 414/516] Bugfix: more process tree fixing (v14.15.2) - bugfix, now giving little bit more stack to process tree --- common/version.go | 2 +- docs/changelog.md | 8 ++++++-- operations/processtree.go | 11 ++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 2876c022..9a5802ee 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.15.1` + Version = `v14.15.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5305ed4d..5bcd57e5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,10 +1,14 @@ # rcc change log -## v14.15.1 (date: 9.8.2023) +## v14.15.2 (date: 10.8.2023) + +- bugfix, now giving little bit more stack to process tree + +## v14.15.1 (date: 9.8.2023) BROKEN - bugfix on process tree ending up eating too much stack (stack overflow) -## v14.15.0 (date: 3.8.2023) +## v14.15.0 (date: 3.8.2023) BROKEN - micromamba upgrade to v1.4.9 diff --git a/operations/processtree.go b/operations/processtree.go index 411b0c7b..bf30508f 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -109,7 +109,7 @@ func SubprocessWarning(seen ChildMap, use bool) error { for pid, executable := range seen { ref, ok := processes[pid] if ok { - updateActiveChildren(ref, masked, 30) + updateActiveChildren(ref, masked, 70) } else { masked[pid] = executable } @@ -145,11 +145,8 @@ func updateActiveChildren(host *ProcessNode, seen ChildMap, maxDepth int) { return } for pid, child := range host.Children { - _, ok := seen[pid] - if !ok { - seen[pid] = child.Executable - updateActiveChildren(child, seen, maxDepth-1) - } + seen[pid] = child.Executable + updateActiveChildren(child, seen, maxDepth-1) } } @@ -157,7 +154,7 @@ func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) { source, ok := processes[pid] if ok { removeStaleChildren(processes, seen) - updateActiveChildren(source, seen, 30) + updateActiveChildren(source, seen, 70) } } From 9c22d61a4aa9313f8b95a1a2a9cfc639b7535a34 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 14 Aug 2023 14:20:06 +0300 Subject: [PATCH 415/516] Improvement: canary failure messages (v14.15.3) - added error message on canary failures - added one diagnostics detail to show if global shared holotree is enabled --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/version.go b/common/version.go index 9a5802ee..5c0805be 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.15.2` + Version = `v14.15.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 5bcd57e5..07f64ece 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v14.15.3 (date: 14.8.2023) + +- added error message on canary failures +- added one diagnostics detail to show if global shared holotree is enabled + ## v14.15.2 (date: 10.8.2023) - bugfix, now giving little bit more stack to process tree diff --git a/operations/diagnostics.go b/operations/diagnostics.go index a6314d08..ee08785d 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -93,6 +93,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["hololib-library-location"] = common.HololibLibraryLocation() result.Details["holotree-location"] = common.HolotreeLocation() result.Details["holotree-shared"] = fmt.Sprintf("%v", common.SharedHolotree) + result.Details["holotree-global-shared"] = fmt.Sprintf("%v", pathlib.IsFile(common.SharedMarkerLocation())) result.Details["holotree-user-id"] = common.UserHomeIdentity() result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) @@ -413,7 +414,7 @@ func condaHeadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkHEAD, Status: statusWarning, - Message: fmt.Sprintf("Conda canary download failed: %d", response.Status), + Message: fmt.Sprintf("Conda canary download failed: %d %v", response.Status, response.Err), Link: supportNetworkUrl, } } @@ -445,7 +446,7 @@ func pypiHeadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkHEAD, Status: statusWarning, - Message: fmt.Sprintf("PyPI canary download failed: %d", response.Status), + Message: fmt.Sprintf("PyPI canary download failed: %d %v", response.Status, response.Err), Link: supportNetworkUrl, } } @@ -477,7 +478,7 @@ func canaryDownloadCheck() *common.DiagnosticCheck { Type: "network", Category: common.CategoryNetworkCanary, Status: statusFail, - Message: fmt.Sprintf("Canary download failed: %d: %s", response.Status, response.Body), + Message: fmt.Sprintf("Canary download failed: %d: %v %s", response.Status, response.Err, response.Body), Link: supportNetworkUrl, } } From c74154fa919f620176adbf5a2804a8e960df5a29 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 17 Aug 2023 09:25:56 +0300 Subject: [PATCH 416/516] Micromamba downgrade v1.4.2 (v14.15.4) --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 5c0805be..13509dc7 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.15.3` + Version = `v14.15.4` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index e35c25af..be94c66e 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_009 - MicromambaVersionNumber = "v1.4.9" + MicromambaVersionLimit = 1_004_002 + MicromambaVersionNumber = "v1.4.2" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index 07f64ece..f8dcdf50 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v14.15.4 (date: 17.8.2023) + +- micromamba downgraded to v1.4.2 due to argument change + ## v14.15.3 (date: 14.8.2023) - added error message on canary failures From 0728f5b3fdd16c9bd7042e832522a0d76e14ada2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 21 Aug 2023 15:16:52 +0300 Subject: [PATCH 417/516] Major breaking changes (v15.0.0) - breaking change: dropped default value `rcc robot initialize --template` option (now it must be given) - breaking change: environment variable `RCC_VERBOSITY` with values "silent", "debug", and "trace" now override CLI options - bugfix, process tree detecting and printing - added debug/trace logging into process baby sitter - work in progress: detecting cacheable environment configurations - micromamba upgrade back to v1.4.9 (next trial) --- cloud/client.go | 2 +- cmd/assistantList.go | 2 +- cmd/assistantRun.go | 4 +- cmd/authorize.go | 2 +- cmd/carrier.go | 2 +- cmd/cleanup.go | 2 +- cmd/cloudNew.go | 2 +- cmd/cloudPrepare.go | 4 +- cmd/communitypull.go | 2 +- cmd/configureexport.go | 2 +- cmd/configureimport.go | 2 +- cmd/configureswitch.go | 2 +- cmd/credentials.go | 2 +- cmd/diagnostics.go | 2 +- cmd/download.go | 4 +- cmd/finder.go | 2 +- cmd/holotreeBlueprints.go | 2 +- cmd/holotreeBootstrap.go | 2 +- cmd/holotreeCatalogs.go | 2 +- cmd/holotreeExport.go | 2 +- cmd/holotreeHash.go | 4 +- cmd/holotreeImport.go | 2 +- cmd/holotreeInit.go | 2 +- cmd/holotreeList.go | 2 +- cmd/holotreePrebuild.go | 2 +- cmd/holotreePull.go | 2 +- cmd/holotreeRemove.go | 2 +- cmd/holotreeShared.go | 2 +- cmd/holotreeStats.go | 2 +- cmd/holotreeVariables.go | 2 +- cmd/initialize.go | 15 +++--- cmd/internale2ee.go | 2 +- cmd/issue.go | 2 +- cmd/metric.go | 2 +- cmd/netdiagnostics.go | 2 +- cmd/pull.go | 4 +- cmd/push.go | 4 +- cmd/rccremote/main.go | 8 ++-- cmd/robotdependencies.go | 2 +- cmd/robotdiagnostics.go | 2 +- cmd/root.go | 11 +++-- cmd/run.go | 2 +- cmd/script.go | 2 +- cmd/shell.go | 2 +- cmd/speed.go | 10 ++-- cmd/testrun.go | 2 +- cmd/unwrap.go | 2 +- cmd/upload.go | 4 +- cmd/userinfo.go | 2 +- cmd/wizardconfig.go | 2 +- cmd/wizardcreate.go | 2 +- cmd/workspace.go | 2 +- cmd/wrap.go | 2 +- common/logger.go | 10 ++-- common/platform_darwin.go | 10 ++++ common/platform_linux.go | 10 ++++ common/platform_windows.go | 10 ++++ common/variables.go | 56 ++++++++++++++++------ common/version.go | 2 +- conda/cacheable.go | 1 + conda/condayaml.go | 29 +++++++++++- conda/condayaml_test.go | 32 +++++++++++++ conda/installing.go | 4 +- conda/platform_windows.go | 2 +- conda/robocorp.go | 4 +- docs/changelog.md | 11 +++++ operations/community.go | 2 +- operations/netdiagnostics.go | 4 +- operations/processtree.go | 90 ++++++++++++++++++++++-------------- operations/running.go | 2 +- operations/updownload.go | 2 +- pathlib/lock_unix.go | 2 +- pathlib/lock_windows.go | 2 +- pretty/functions.go | 5 ++ pretty/variables.go | 4 +- robot_tests/fullrun.robot | 2 +- 76 files changed, 305 insertions(+), 147 deletions(-) create mode 100644 conda/cacheable.go diff --git a/cloud/client.go b/cloud/client.go index d92bb6e0..c98bb33c 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -161,7 +161,7 @@ func (it *internalClient) does(method string, request *Request) *Response { } else { response.Body, response.Err = io.ReadAll(httpResponse.Body) } - if common.DebugFlag { + if common.DebugFlag() { body := "ignore" if response.Status > 399 { body = string(response.Body) diff --git a/cmd/assistantList.go b/cmd/assistantList.go index c37b34cd..efc37e94 100644 --- a/cmd/assistantList.go +++ b/cmd/assistantList.go @@ -17,7 +17,7 @@ var assistantListCmd = &cobra.Command{ Short: "Robot Assistant listing", Long: "Robot Assistant listing.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Robot Assistant list query lasted").Report() } account := operations.AccountByName(AccountName()) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 754255ee..0c59d19f 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -30,7 +30,7 @@ var assistantRunCmd = &cobra.Command{ var status, reason string status, reason = "ERROR", "UNKNOWN" elapser := common.Stopwatch("Robot Assistant startup lasted") - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Robot Assistant run lasted").Report() } account := operations.AccountByName(AccountName()) @@ -85,7 +85,7 @@ var assistantRunCmd = &cobra.Command{ defer pathlib.Walk(artifactDir, pathlib.IgnoreOlder(sentinelTime).Ignore, TargetDir(copyDirectory).OverwriteBack) } } - if common.DebugFlag { + if common.DebugFlag() { elapser.Report() } diff --git a/cmd/authorize.go b/cmd/authorize.go index bd992c49..c2f00a4f 100644 --- a/cmd/authorize.go +++ b/cmd/authorize.go @@ -15,7 +15,7 @@ var authorizeCmd = &cobra.Command{ Short: "Convert an API key to a valid authorization JWT token.", Long: "Convert an API key to a valid authorization JWT token.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Authorize query lasted").Report() } period := &operations.TokenPeriod{ diff --git a/cmd/carrier.go b/cmd/carrier.go index 452bd133..f3d5461e 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -38,7 +38,7 @@ func runCarrier() error { if !ok { return fmt.Errorf("This executable is not carrier!") } - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Task testrun lasted").Report() } now := time.Now() diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 24a79419..994aeede 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -22,7 +22,7 @@ var cleanupCmd = &cobra.Command{ Long: `Cleanup removes old virtual environments from existence. After cleanup, they will not be available anymore.`, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Env cleanup lasted").Report() } err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag, downloadsFlag) diff --git a/cmd/cloudNew.go b/cmd/cloudNew.go index 68d66602..dace06a0 100644 --- a/cmd/cloudNew.go +++ b/cmd/cloudNew.go @@ -18,7 +18,7 @@ var newCloudCmd = &cobra.Command{ Short: "Create a new robot into Robocorp Control Room.", Long: "Create a new robot into Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("New robot creation lasted").Report() } account := operations.AccountByName(AccountName()) diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index b44c9fd3..4737aa28 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -23,7 +23,7 @@ var prepareCloudCmd = &cobra.Command{ Long: "Prepare cloud robot for fast startup time in local computer.", Run: func(cmd *cobra.Command, args []string) { defer journal.BuildEventStats("prepare") - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Cloud prepare lasted").Report() } @@ -39,7 +39,7 @@ var prepareCloudCmd = &cobra.Command{ client, err := cloud.NewClient(account.Endpoint) pretty.Guard(err == nil, 3, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) - err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) pretty.Guard(err == nil, 4, "Error: %v", err) common.Debug("Using temporary workarea: %v", workarea) diff --git a/cmd/communitypull.go b/cmd/communitypull.go index e9b0869f..1f781fbd 100644 --- a/cmd/communitypull.go +++ b/cmd/communitypull.go @@ -23,7 +23,7 @@ var communityPullCmd = &cobra.Command{ Long: "Pull a robot from URL or community sources.", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Pull lasted").Report() } diff --git a/cmd/configureexport.go b/cmd/configureexport.go index f20374e1..c7103e4b 100644 --- a/cmd/configureexport.go +++ b/cmd/configureexport.go @@ -18,7 +18,7 @@ var configureExportCmd = &cobra.Command{ Short: "Export a configuration profile for Robocorp tooling.", Long: "Export a configuration profile for Robocorp tooling.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Configuration export lasted").Report() } profile := loadNamedProfile(profileName) diff --git a/cmd/configureimport.go b/cmd/configureimport.go index e480c061..7c30fcaa 100644 --- a/cmd/configureimport.go +++ b/cmd/configureimport.go @@ -17,7 +17,7 @@ var configureImportCmd = &cobra.Command{ Short: "Import a configuration profile for Robocorp tooling.", Long: "Import a configuration profile for Robocorp tooling.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Configuration import lasted").Report() } profile := &settings.Profile{} diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go index 1f25dc81..f79813bb 100644 --- a/cmd/configureswitch.go +++ b/cmd/configureswitch.go @@ -73,7 +73,7 @@ var configureSwitchCmd = &cobra.Command{ Short: "Switch active configuration profile for Robocorp tooling.", Long: "Switch active configuration profile for Robocorp tooling.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Configuration switch lasted").Report() } if clearProfile { diff --git a/cmd/credentials.go b/cmd/credentials.go index 4e60bad3..6b5eb151 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -23,7 +23,7 @@ var credentialsCmd = &cobra.Command{ Long: "Manage Robocorp Control Room API credentials for later use.", Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Credentials query lasted").Report() } var account, credentials, endpoint string diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index c323f424..7e4f4be2 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -20,7 +20,7 @@ var diagnosticsCmd = &cobra.Command{ Short: "Run system diagnostics to help resolve rcc issues.", Long: "Run system diagnostics to help resolve rcc issues.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Diagnostic run lasted").Report() } _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag, quickFilterFlag) diff --git a/cmd/download.go b/cmd/download.go index b033bb82..abff15ab 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -14,7 +14,7 @@ var downloadCmd = &cobra.Command{ Short: "Fetch an existing robot from Robocorp Control Room.", Long: "Fetch an existing robot from Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Download lasted").Report() } account := operations.AccountByName(AccountName()) @@ -25,7 +25,7 @@ var downloadCmd = &cobra.Command{ if err != nil { pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } - err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/cmd/finder.go b/cmd/finder.go index 967e8f80..ff951279 100644 --- a/cmd/finder.go +++ b/cmd/finder.go @@ -20,7 +20,7 @@ Example: rcc internal finder -d /starting/path/somewhere robot.yaml`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Finder run lasted").Report() } found, err := pathlib.FindNamedPath(shellDirectory, args[0]) diff --git a/cmd/holotreeBlueprints.go b/cmd/holotreeBlueprints.go index 48fd7551..a3525fb0 100644 --- a/cmd/holotreeBlueprints.go +++ b/cmd/holotreeBlueprints.go @@ -33,7 +33,7 @@ var holotreeBlueprintCmd = &cobra.Command{ Long: "Verify that resulting blueprint is in hololibrary.", Aliases: []string{"bp"}, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree blueprints command lasted").Report() } diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index af5c174a..900985f2 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -49,7 +49,7 @@ var holotreeBootstrapCmd = &cobra.Command{ Long: "Bootstrap holotree from set of templates.", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree bootstrap lasted").Report() } diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index 302d0c4f..29de786c 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -162,7 +162,7 @@ var holotreeCatalogsCmd = &cobra.Command{ Short: "List native and imported holotree catalogs.", Long: "List native and imported holotree catalogs.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree catalogs command lasted").Report() } _, roots := htfs.LoadCatalogs() diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index a26d074c..adbdb8ce 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -66,7 +66,7 @@ var holotreeExportCmd = &cobra.Command{ Short: "Export existing holotree catalog and library parts.", Long: "Export existing holotree catalog and library parts.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree export command lasted").Report() } if len(exportRobot) > 0 { diff --git a/cmd/holotreeHash.go b/cmd/holotreeHash.go index a52fc5d8..4884846f 100644 --- a/cmd/holotreeHash.go +++ b/cmd/holotreeHash.go @@ -14,14 +14,14 @@ var holotreeHashCmd = &cobra.Command{ Long: "Calculates a blueprint hash for managed holotree virtual environment from conda.yaml files.", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Conda YAML hash calculation lasted").Report() } _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(args, "") pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) hash := common.BlueprintHash(holotreeBlueprint) common.Log("Blueprint hash for %v is %v.", args, hash) - if common.Silent { + if common.Silent() { common.Stdout("%s\n", hash) } }, diff --git a/cmd/holotreeImport.go b/cmd/holotreeImport.go index 33a4b888..3379b937 100644 --- a/cmd/holotreeImport.go +++ b/cmd/holotreeImport.go @@ -54,7 +54,7 @@ var holotreeImportCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { var err error - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree import command lasted").Report() } for at, filename := range args { diff --git a/cmd/holotreeInit.go b/cmd/holotreeInit.go index 01a19a53..47111aff 100644 --- a/cmd/holotreeInit.go +++ b/cmd/holotreeInit.go @@ -37,7 +37,7 @@ var holotreeInitCmd = &cobra.Command{ Short: "Initialize shared holotree location.", Long: "Initialize shared holotree location.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Initialize shared holotree location lasted").Report() } pretty.Warning("Running this command might need 'rcc holotree shared --enable' first. Still, trying ...") diff --git a/cmd/holotreeList.go b/cmd/holotreeList.go index c9bb7837..34f347c2 100644 --- a/cmd/holotreeList.go +++ b/cmd/holotreeList.go @@ -54,7 +54,7 @@ var holotreeListCmd = &cobra.Command{ Short: "List holotree spaces.", Long: "List holotree spaces.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree list lasted").Report() } diff --git a/cmd/holotreePrebuild.go b/cmd/holotreePrebuild.go index 2b00c08d..14f4b302 100644 --- a/cmd/holotreePrebuild.go +++ b/cmd/holotreePrebuild.go @@ -102,7 +102,7 @@ var holotreePrebuildCmd = &cobra.Command{ Long: "Prebuild hololib from given set of environment descriptors. Requires shared holotree to be enabled and active.", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree prebuild lasted").Report() } diff --git a/cmd/holotreePull.go b/cmd/holotreePull.go index b668e00e..a4688d5d 100644 --- a/cmd/holotreePull.go +++ b/cmd/holotreePull.go @@ -19,7 +19,7 @@ var holotreePullCmd = &cobra.Command{ Short: "Try to pull existing holotree catalog from remote source.", Long: "Try to pull existing holotree catalog from remote source.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree pull command lasted").Report() } _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(nil, pullRobot) diff --git a/cmd/holotreeRemove.go b/cmd/holotreeRemove.go index 89aeca14..01f5ab93 100644 --- a/cmd/holotreeRemove.go +++ b/cmd/holotreeRemove.go @@ -46,7 +46,7 @@ var holotreeRemoveCmd = &cobra.Command{ Long: "Remove existing holotree catalogs. Partial identities are ok.", Aliases: []string{"rm"}, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree remove command lasted").Report() } if unusedDays > 0 { diff --git a/cmd/holotreeShared.go b/cmd/holotreeShared.go index 29409b28..f381af20 100644 --- a/cmd/holotreeShared.go +++ b/cmd/holotreeShared.go @@ -20,7 +20,7 @@ var holotreeSharedCommand = &cobra.Command{ Short: "Enable shared holotree usage.", Long: "Enable shared holotree usage.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Enabling shared holotree lasted").Report() } enabled := pathlib.IsFile(common.SharedMarkerLocation()) diff --git a/cmd/holotreeStats.go b/cmd/holotreeStats.go index 90fe8983..5df8d637 100644 --- a/cmd/holotreeStats.go +++ b/cmd/holotreeStats.go @@ -22,7 +22,7 @@ var holotreeStatsCmd = &cobra.Command{ Long: "Show holotree environment build and runtime statistics.", Aliases: []string{"statistic", "stats", "stat", "st"}, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree stats calculation lasted").Report() } journal.ShowStatistics(statsWeeks, onlyAssistantStats, onlyRobotStats, onlyPrepareStats, onlyVariablesStats) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index d01748ee..9e8f6663 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -132,7 +132,7 @@ var holotreeVariablesCmd = &cobra.Command{ Long: "Do holotree operations.", Run: func(cmd *cobra.Command, args []string) { defer journal.BuildEventStats("variables") - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Holotree variables command lasted").Report() } diff --git a/cmd/initialize.go b/cmd/initialize.go index 71d18c98..ec12e339 100644 --- a/cmd/initialize.go +++ b/cmd/initialize.go @@ -13,13 +13,10 @@ var ( ) func createWorkarea() { - if len(directory) == 0 { - pretty.Exit(1, "Error: missing target directory") - } + pretty.Guard(len(directory) > 0, 1, "Error: missing target directory (option: --directory)") + pretty.Guard(len(templateName) > 0, 3, "Error: missing template name (option: --template)") err := operations.InitializeWorkarea(directory, templateName, internalOnlyFlag, forceFlag) - if err != nil { - pretty.Exit(2, "Error: %v", err) - } + pretty.Guard(err == nil, 2, "Error: %v", err) } func listJsonTemplates() { @@ -45,7 +42,7 @@ var initializeCmd = &cobra.Command{ Short: "Create a directory structure for a robot.", Long: "Create a directory structure for a robot.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Initialization lasted").Report() } if jsonFlag { @@ -64,9 +61,9 @@ var initializeCmd = &cobra.Command{ func init() { robotCmd.AddCommand(initializeCmd) initializeCmd.Flags().StringVarP(&directory, "directory", "d", ".", "Root directory to create the new robot in.") - initializeCmd.Flags().StringVarP(&templateName, "template", "t", "standard", "Template to use to generate the robot content.") + initializeCmd.Flags().StringVarP(&templateName, "template", "t", "", "Template to use to generate the robot content.") initializeCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force the creation of the robot and possibly overwrite data.") initializeCmd.Flags().BoolVarP(&listFlag, "list", "l", false, "List available templates.") initializeCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "List available templates as JSON.") - initializeCmd.Flags().BoolVarP(&internalOnlyFlag, "internal", "i", false, "Use only builtin internal templates.") + initializeCmd.Flags().BoolVarP(&internalOnlyFlag, "internal", "i", false, "Undocumented. Deprecated. DO NOT USE.") } diff --git a/cmd/internale2ee.go b/cmd/internale2ee.go index cf8ab28d..a0a58f33 100644 --- a/cmd/internale2ee.go +++ b/cmd/internale2ee.go @@ -21,7 +21,7 @@ var e2eeCmd = &cobra.Command{ Long: "Internal end-to-end encryption tester method", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Encryption lasted").Report() } if encryptionVersion == 1 { diff --git a/cmd/issue.go b/cmd/issue.go index fce57df1..19e9e0b4 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -19,7 +19,7 @@ var issueCmd = &cobra.Command{ Short: "Send an issue to Robocorp Control Room via rcc.", Long: "Send an issue to Robocorp Control Room via rcc.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Feedback issue lasted").Report() } accountEmail := "unknown" diff --git a/cmd/metric.go b/cmd/metric.go index af2197e5..43477b4f 100644 --- a/cmd/metric.go +++ b/cmd/metric.go @@ -20,7 +20,7 @@ var metricCmd = &cobra.Command{ Short: "Send some metric to Robocorp Control Room.", Long: "Send some metric to Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Feedback metric lasted").Report() } if !xviper.CanTrack() { diff --git a/cmd/netdiagnostics.go b/cmd/netdiagnostics.go index 41698b1c..c746e67f 100644 --- a/cmd/netdiagnostics.go +++ b/cmd/netdiagnostics.go @@ -29,7 +29,7 @@ var netDiagnosticsCmd = &cobra.Command{ Short: "Run additional diagnostics to help resolve network issues.", Long: "Run additional diagnostics to help resolve network issues.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Netdiagnostic run lasted").Report() } config, err := summonNetworkDiagConfig(netConfigFilename) diff --git a/cmd/pull.go b/cmd/pull.go index 0f1c7f8e..6978aca2 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -19,7 +19,7 @@ var pullCmd = &cobra.Command{ Short: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", Long: "Pull a robot from Robocorp Control Room and unwrap it into local directory.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Pull lasted").Report() } @@ -37,7 +37,7 @@ var pullCmd = &cobra.Command{ defer os.Remove(zipfile) common.Debug("Using temporary zipfile at %v", zipfile) - err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err = operations.DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/cmd/push.go b/cmd/push.go index c084213a..646709e0 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -19,7 +19,7 @@ var pushCmd = &cobra.Command{ Short: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", Long: "Wrap the local directory and push it into Robocorp Control Room as a specific robot.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Push lasted").Report() } account := operations.AccountByName(AccountName()) @@ -39,7 +39,7 @@ var pushCmd = &cobra.Command{ if err != nil { pretty.Exit(3, "Error: %v", err) } - err = operations.UploadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err = operations.UploadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) if err != nil { pretty.Exit(4, "Error: %v", err) } diff --git a/cmd/rccremote/main.go b/cmd/rccremote/main.go index 933cc4fc..72f9a67b 100644 --- a/cmd/rccremote/main.go +++ b/cmd/rccremote/main.go @@ -17,6 +17,8 @@ var ( serverPort int versionFlag bool holdingArea string + debugFlag bool + traceFlag bool ) func defaultHoldLocation() string { @@ -28,8 +30,8 @@ func defaultHoldLocation() string { } func init() { - flag.BoolVar(&common.DebugFlag, "debug", false, "Turn on debugging output.") - flag.BoolVar(&common.TraceFlag, "trace", false, "Turn on tracing output.") + flag.BoolVar(&debugFlag, "debug", false, "Turn on debugging output.") + flag.BoolVar(&traceFlag, "trace", false, "Turn on tracing output.") flag.BoolVar(&versionFlag, "version", false, "Just show rccremote version and exit.") flag.StringVar(&serverName, "hostname", "localhost", "Hostname/address to bind server to.") @@ -72,6 +74,6 @@ func main() { pretty.Setup() flag.Parse() - common.UnifyVerbosityFlags() + common.DefineVerbosity(false, debugFlag, traceFlag) process() } diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index fe5f0b4e..762a1db7 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -39,7 +39,7 @@ var robotDependenciesCmd = &cobra.Command{ Long: "View wanted vs. available dependencies of robot execution environment.", Aliases: []string{"deps"}, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Robot dependencies run lasted").Report() } simple, config, _, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) diff --git a/cmd/robotdiagnostics.go b/cmd/robotdiagnostics.go index 7054d53c..eea156a8 100644 --- a/cmd/robotdiagnostics.go +++ b/cmd/robotdiagnostics.go @@ -13,7 +13,7 @@ var robotDiagnosticsCmd = &cobra.Command{ Short: "Run system diagnostics to help resolve rcc issues.", Long: "Run system diagnostics to help resolve rcc issues.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Diagnostic run lasted").Report() } err := operations.PrintRobotDiagnostics(robotFile, jsonFlag, productionFlag) diff --git a/cmd/root.go b/cmd/root.go index 5f714395..0b6d08e6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,9 @@ var ( profilefile string profiling *os.File versionFlag bool + silentFlag bool + debugFlag bool + traceFlag bool ) func toplevelCommands(parent *cobra.Command) { @@ -106,15 +109,15 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib") - rootCmd.PersistentFlags().BoolVarP(&common.Silent, "silent", "", false, "be less verbose on output") + rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output") rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") rootCmd.PersistentFlags().BoolVarP(&pathlib.Lockless, "lockless", "", false, "do not use file locking ... DANGER!") rootCmd.PersistentFlags().BoolVarP(&pretty.Colorless, "colorless", "", false, "do not use colors in CLI UI") rootCmd.PersistentFlags().BoolVarP(&common.NoCache, "nocache", "", false, "do not use cache for credentials and tokens, always request them from cloud") rootCmd.PersistentFlags().BoolVarP(&common.LogLinenumbers, "numbers", "", false, "put line numbers on rcc produced log output") - rootCmd.PersistentFlags().BoolVarP(&common.DebugFlag, "debug", "", false, "to get debug output where available (not for production use)") - rootCmd.PersistentFlags().BoolVarP(&common.TraceFlag, "trace", "", false, "to get trace output where available (not for production use)") + rootCmd.PersistentFlags().BoolVarP(&debugFlag, "debug", "", false, "to get debug output where available (not for production use)") + rootCmd.PersistentFlags().BoolVarP(&traceFlag, "trace", "", false, "to get trace output where available (not for production use)") rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") @@ -137,7 +140,7 @@ func initConfig() { xviper.SetConfigFile(filepath.Join(common.RobocorpHome(), "rcc.yaml")) } - common.UnifyVerbosityFlags() + common.DefineVerbosity(silentFlag, debugFlag, traceFlag) common.UnifyStageHandling() pretty.Setup() diff --git a/cmd/run.go b/cmd/run.go index 9e6be0e5..92977b2f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -25,7 +25,7 @@ in your own machine.`, Run: func(cmd *cobra.Command, args []string) { defer conda.RemoveCurrentTemp() defer journal.BuildEventStats("robot") - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Task run lasted").Report() } simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) diff --git a/cmd/script.go b/cmd/script.go index e8406975..1d768373 100644 --- a/cmd/script.go +++ b/cmd/script.go @@ -17,7 +17,7 @@ var scriptCmd = &cobra.Command{ `, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Task run lasted").Report() } simple, config, todo, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) diff --git a/cmd/shell.go b/cmd/shell.go index 6a1f1565..1f50a4e4 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -18,7 +18,7 @@ It can be used to get inside a managed environment and execute your own command within that environment.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("rcc shell lasted").Report() } simple, config, todo, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) diff --git a/cmd/speed.go b/cmd/speed.go index 7a14c5f1..ec5dcacd 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -50,18 +50,17 @@ var speedtestCmd = &cobra.Command{ Short: "Run system speed test to find how rcc performs in your system.", Long: "Run system speed test to find how rcc performs in your system.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Speed test run lasted").Report() } common.Log("Running network and filesystem performance tests with %d workers.", anywork.Scale()) common.Log("This may take several minutes, please be patient.") signal := make(chan bool) timing := make(chan int) - silent, trace, debug := common.Silent, common.TraceFlag, common.DebugFlag + silent, trace, debug := common.Silent(), common.TraceFlag(), common.DebugFlag() if !debug { go workingWorm(signal, timing) - common.Silent = true - common.UnifyVerbosityFlags() + common.DefineVerbosity(true, false, false) } folder := common.RobocorpTemp() content, err := blobs.Asset("assets/speedtest.yaml") @@ -75,8 +74,7 @@ var speedtestCmd = &cobra.Command{ } common.ForcedRobocorpHome = folder _, score, err := htfs.NewEnvironment(condafile, "", true, true, operations.PullCatalog) - common.Silent, common.TraceFlag, common.DebugFlag = silent, trace, debug - common.UnifyVerbosityFlags() + common.DefineVerbosity(silent, debug, trace) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/cmd/testrun.go b/cmd/testrun.go index b415b463..0bf124a0 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -24,7 +24,7 @@ var testrunCmd = &cobra.Command{ Long: "Run a task in a clean environment and clean directory.", Run: func(cmd *cobra.Command, args []string) { defer conda.RemoveCurrentTemp() - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Task testrun lasted").Report() } now := time.Now() diff --git a/cmd/unwrap.go b/cmd/unwrap.go index 941095d9..5c747956 100644 --- a/cmd/unwrap.go +++ b/cmd/unwrap.go @@ -15,7 +15,7 @@ var unwrapCmd = &cobra.Command{ robot filename, and target directory. And using --force option, files will be overwritten.`, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Unwrap lasted").Report() } err := operations.Unzip(directory, zipfile, forceFlag, false, true) diff --git a/cmd/upload.go b/cmd/upload.go index 8fb8e5f3..e94b47af 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -14,7 +14,7 @@ var uploadCmd = &cobra.Command{ Short: "Push an existing robot to Robocorp Control Room.", Long: "Push an existing robot to Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Upload lasted").Report() } account := operations.AccountByName(AccountName()) @@ -25,7 +25,7 @@ var uploadCmd = &cobra.Command{ if err != nil { pretty.Exit(2, "Could not create client for endpoint: %v, reason: %v", account.Endpoint, err) } - err = operations.UploadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err = operations.UploadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) if err != nil { pretty.Exit(3, "Error: %v", err) } diff --git a/cmd/userinfo.go b/cmd/userinfo.go index 570c29a9..286e5ac6 100644 --- a/cmd/userinfo.go +++ b/cmd/userinfo.go @@ -17,7 +17,7 @@ var userinfoCmd = &cobra.Command{ Short: "Query user information from Robocorp Control Room.", Long: "Query user information from Robocorp Control Room.", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Userinfo query lasted").Report() } account := operations.AccountByName(AccountName()) diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go index ec093bff..2ada7106 100644 --- a/cmd/wizardconfig.go +++ b/cmd/wizardconfig.go @@ -17,7 +17,7 @@ var wizardConfigCommand = &cobra.Command{ if !pretty.Interactive { pretty.Exit(1, "This is for interactive use only. Do not use in scripting/CI!") } - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Interactive configuration lasted").Report() } err := wizard.Configure(args) diff --git a/cmd/wizardcreate.go b/cmd/wizardcreate.go index ca9c648b..ea918632 100644 --- a/cmd/wizardcreate.go +++ b/cmd/wizardcreate.go @@ -16,7 +16,7 @@ var wizardCreateCmd = &cobra.Command{ if !pretty.Interactive { pretty.Exit(1, "This is for interactive use only. Do not use in scripting/CI!") } - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Interactive create lasted").Report() } err := wizard.Create(args) diff --git a/cmd/workspace.go b/cmd/workspace.go index 8e2499ce..64f1fab0 100644 --- a/cmd/workspace.go +++ b/cmd/workspace.go @@ -16,7 +16,7 @@ var workspaceCmd = &cobra.Command{ Short: "List the available workspaces and their tasks (with --workspace option).", Long: "List the available workspaces and their tasks (with --workspace option).", Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Workspace query lasted").Report() } account := operations.AccountByName(AccountName()) diff --git a/cmd/wrap.go b/cmd/wrap.go index d3b735b4..b2bd352d 100644 --- a/cmd/wrap.go +++ b/cmd/wrap.go @@ -15,7 +15,7 @@ var wrapCmd = &cobra.Command{ filename, source directory and optional ignore files. When wrap is run again existing robot file will silently be overwritten..`, Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Wrap lasted").Report() } err := operations.Zip(directory, zipfile, ignores) diff --git a/common/logger.go b/common/logger.go index 7d9a7cf0..8cb137a0 100644 --- a/common/logger.go +++ b/common/logger.go @@ -27,7 +27,7 @@ func loggerLoop(writers logwriters) { } out, message := todo() - if TraceFlag { + if TraceFlag() { stamp = time.Now().Format("02.150405.000 ") } else if LogLinenumbers { stamp = fmt.Sprintf("%3d ", line) @@ -64,9 +64,9 @@ func Error(context string, err error) { } func Log(format string, details ...interface{}) { - if !Silent { + if !Silent() { prefix := "" - if DebugFlag || TraceFlag { + if DebugFlag() || TraceFlag() { prefix = "[N] " } printout(os.Stderr, fmt.Sprintf(prefix+format, details...)) @@ -74,14 +74,14 @@ func Log(format string, details ...interface{}) { } func Debug(format string, details ...interface{}) error { - if DebugFlag { + if DebugFlag() { printout(os.Stderr, fmt.Sprintf("[D] "+format, details...)) } return nil } func Trace(format string, details ...interface{}) error { - if TraceFlag { + if TraceFlag() { printout(os.Stderr, fmt.Sprintf("[T] "+format, details...)) } return nil diff --git a/common/platform_darwin.go b/common/platform_darwin.go index 85661e26..0b3ec4b8 100644 --- a/common/platform_darwin.go +++ b/common/platform_darwin.go @@ -1,8 +1,10 @@ package common import ( + "fmt" "os" "path/filepath" + "strings" ) const ( @@ -18,3 +20,11 @@ func ExpandPath(entry string) string { } return result } + +func GenerateKillCommand(keys []int) string { + command := []string{"kill -9"} + for _, key := range keys { + command = append(command, fmt.Sprintf("%d", key)) + } + return strings.Join(command, " ") +} diff --git a/common/platform_linux.go b/common/platform_linux.go index 9e62374f..0af598ca 100644 --- a/common/platform_linux.go +++ b/common/platform_linux.go @@ -1,8 +1,10 @@ package common import ( + "fmt" "os" "path/filepath" + "strings" ) const ( @@ -18,3 +20,11 @@ func ExpandPath(entry string) string { } return result } + +func GenerateKillCommand(keys []int) string { + command := []string{"kill -9"} + for _, key := range keys { + command = append(command, fmt.Sprintf("%d", key)) + } + return strings.Join(command, " ") +} diff --git a/common/platform_windows.go b/common/platform_windows.go index c250746c..47109bf4 100644 --- a/common/platform_windows.go +++ b/common/platform_windows.go @@ -1,9 +1,11 @@ package common import ( + "fmt" "os" "path/filepath" "regexp" + "strings" ) const ( @@ -36,3 +38,11 @@ func fromEnvironment(form string) string { } return form } + +func GenerateKillCommand(keys []int) string { + command := []string{"taskkill /f"} + for _, key := range keys { + command = append(command, fmt.Sprintf("/pid %d", key)) + } + return strings.Join(command, " ") +} diff --git a/common/variables.go b/common/variables.go index 253ea0b8..11072aa8 100644 --- a/common/variables.go +++ b/common/variables.go @@ -10,19 +10,32 @@ import ( "time" ) +type ( + Verbosity uint8 +) + +const ( + Undefined Verbosity = 0 + Silently Verbosity = 1 + Normal Verbosity = 2 + Debugging Verbosity = 3 + Tracing Verbosity = 4 +) + const ( ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` RCC_REMOTE_ORIGIN = `RCC_REMOTE_ORIGIN` RCC_REMOTE_AUTHORIZATION = `RCC_REMOTE_AUTHORIZATION` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` + RCC_VERBOSITY = `RCC_VERBOSITY` + SILENTLY = `silent` + TRACING = `trace` + DEBUGGING = `debug` ) var ( NoBuild bool - Silent bool - DebugFlag bool - TraceFlag bool DeveloperFlag bool StrictFlag bool SharedHolotree bool @@ -43,6 +56,7 @@ var ( ProgressMark time.Time Clock *stopwatch randomIdentifier string + verbosity Verbosity ) func init() { @@ -96,8 +110,16 @@ func RobocorpLock() string { return filepath.Join(RobocorpHome(), "robocorp.lck") } +func DebugFlag() bool { + return verbosity >= Debugging +} + +func TraceFlag() bool { + return verbosity >= Tracing +} + func VerboseEnvironmentBuilding() bool { - return DebugFlag || TraceFlag || len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 + return DebugFlag() || TraceFlag() || len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 } func OverrideSystemRequirements() bool { @@ -258,16 +280,24 @@ func CaBundleFile() string { return ExpandPath(filepath.Join(RobocorpHome(), "ca-bundle.pem")) } -func UnifyVerbosityFlags() { - if Silent { - DebugFlag = false - TraceFlag = false - } - if TraceFlag { - DebugFlag = true +func DefineVerbosity(silent, debug, trace bool) { + override := os.Getenv(RCC_VERBOSITY) + switch { + case silent || override == SILENTLY: + verbosity = Silently + case trace || override == TRACING: + verbosity = Tracing + case debug || override == DEBUGGING: + verbosity = Debugging + default: + verbosity = Normal } } +func Silent() bool { + return verbosity == Silently +} + func UnifyStageHandling() { if len(StageFolder) > 0 { Liveonly = true @@ -275,9 +305,7 @@ func UnifyStageHandling() { } func ForceDebug() { - Silent = false - DebugFlag = true - UnifyVerbosityFlags() + DefineVerbosity(false, true, false) } func Platform() string { diff --git a/common/version.go b/common/version.go index 13509dc7..853cf2e4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v14.15.4` + Version = `v15.0.0` ) diff --git a/conda/cacheable.go b/conda/cacheable.go new file mode 100644 index 00000000..e38064af --- /dev/null +++ b/conda/cacheable.go @@ -0,0 +1 @@ +package conda diff --git a/conda/condayaml.go b/conda/condayaml.go index c2a0ee5d..c3512704 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -13,8 +13,17 @@ import ( "gopkg.in/yaml.v2" ) +const ( + alternative = `|` + reject_chars = `(?:[][(){}%/:,;@*<=>!]+)` + and_or = `\b(?:or|and)\b` + dash_start = `^-+` + uncacheableForm = reject_chars + alternative + and_or + alternative + dash_start +) + var ( - dependencyPattern = regexp.MustCompile("^([^<=~!> ]+)\\s*(?:([<=~!>]*)\\s*(\\S+.*?))?$") + dependencyPattern = regexp.MustCompile("^([^<=~!> ]+)\\s*(?:([<=~!>]*)\\s*(\\S+.*?))?$") + uncacheablePattern = regexp.MustCompile(uncacheableForm) ) type internalEnvironment struct { @@ -55,11 +64,29 @@ func AsDependency(value string) *Dependency { } } +func IsCacheable(text string) bool { + flat := strings.TrimSpace(text) + return !uncacheablePattern.MatchString(flat) +} + func (it *Dependency) Representation() string { parts := strings.SplitN(strings.ToLower(it.Name), "[", 2) return parts[0] } +func (it *Dependency) IsCacheable() bool { + if !it.IsExact() { + return false + } + if !IsCacheable(it.Name) { + return false + } + if !IsCacheable(it.Versions) { + return false + } + return true +} + func (it *Dependency) IsExact() bool { return len(it.Qualifier)+len(it.Versions) > 0 } diff --git a/conda/condayaml_test.go b/conda/condayaml_test.go index aad11a50..87b3094d 100644 --- a/conda/condayaml_test.go +++ b/conda/condayaml_test.go @@ -144,3 +144,35 @@ func TestCanGetLayersFromCondaYaml(t *testing.T) { must_be.Equal("5be3e197c8c2c67d", fingerprints[1]) must_be.Equal("d310697aca0840a1", fingerprints[2]) } + +func TestCacheability(t *testing.T) { + must_be, wont_be := hamlet.Specifications(t) + + // some are from https://peps.python.org/pep-0508/ + + must_be.True(conda.IsCacheable("A.B-C_D")) + must_be.True(conda.IsCacheable("simple")) + must_be.True(conda.IsCacheable("simple space separated")) // by itself, ok + must_be.True(conda.IsCacheable("simple-parts")) + must_be.True(conda.IsCacheable("simple_parts")) + must_be.True(conda.IsCacheable("1.2.3")) + must_be.True(conda.IsCacheable("2023c")) + must_be.True(conda.IsCacheable("2023.3")) + must_be.True(conda.IsCacheable("0.1.0.post0")) + + wont_be.True(conda.IsCacheable("a,b")) + wont_be.True(conda.IsCacheable("simple or not")) + wont_be.True(conda.IsCacheable("simple and other")) + wont_be.True(conda.IsCacheable("-simple")) + wont_be.True(conda.IsCacheable(" -simple")) + wont_be.True(conda.IsCacheable("-c constraints.txt")) + wont_be.True(conda.IsCacheable("-r requirements.txt")) + wont_be.True(conda.IsCacheable("simple*")) + wont_be.True(conda.IsCacheable("3.5.*")) + wont_be.True(conda.IsCacheable("name@http://foo.com")) + wont_be.True(conda.IsCacheable("requests[security]")) + wont_be.True(conda.IsCacheable("./downloads/numpy-1.9.2-cp34-none-win32.whl")) + wont_be.True(conda.IsCacheable("urllib3 @ https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip")) + wont_be.True(conda.IsCacheable("urllib3@https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip")) + wont_be.True(conda.IsCacheable("https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip")) +} diff --git a/conda/installing.go b/conda/installing.go index c7d632a3..b95c9bd4 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -13,7 +13,7 @@ func MustMicromamba() bool { } func DoDownload(delay time.Duration) bool { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Download done in").Report() } @@ -32,7 +32,7 @@ func DoDownload(delay time.Duration) bool { } func DoInstall() bool { - if common.DebugFlag { + if common.DebugFlag() { defer common.Stopwatch("Installation done in").Report() } diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 86090914..d1011e80 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -22,7 +22,7 @@ const ( binSuffix = "\\bin" activateScript = "@echo off\n" + "set \"MAMBA_ROOT_PREFIX={{.MambaRootPrefix}}\"\n" + - "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell -s cmd.exe activate -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell activate -s cmd.exe -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + "call \"{{.Rcc}}\" internal env -l after\n" commandSuffix = ".cmd" ) diff --git a/conda/robocorp.go b/conda/robocorp.go index be94c66e..e35c25af 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_002 - MicromambaVersionNumber = "v1.4.2" + MicromambaVersionLimit = 1_004_009 + MicromambaVersionNumber = "v1.4.9" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index f8dcdf50..df331e9e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,16 @@ # rcc change log +## v15.0.0 (date: 21.8.2023) WORK IN PROGRESS + +- breaking change: dropped default value `rcc robot initialize --template` + option (now it must be given) +- breaking change: environment variable `RCC_VERBOSITY` with values "silent", + "debug", and "trace" now override CLI options +- bugfix, process tree detecting and printing +- added debug/trace logging into process baby sitter +- work in progress: detecting cacheable environment configurations +- micromamba upgrade back to v1.4.9 (next trial) + ## v14.15.4 (date: 17.8.2023) - micromamba downgraded to v1.4.2 due to argument change diff --git a/operations/community.go b/operations/community.go index c7a4c39a..2bed8a2e 100644 --- a/operations/community.go +++ b/operations/community.go @@ -74,7 +74,7 @@ func DownloadCommunityRobot(url, filename string) error { return err } - if common.DebugFlag { + if common.DebugFlag() { sum := fmt.Sprintf("%02x", digest.Sum(nil)) common.Debug("SHA256 sum: %s", sum) } diff --git a/operations/netdiagnostics.go b/operations/netdiagnostics.go index c73cfea4..001c3da7 100644 --- a/operations/netdiagnostics.go +++ b/operations/netdiagnostics.go @@ -141,7 +141,7 @@ func headRequest(link string) (code int, fingerprint string, err error) { client, err := cloud.NewClient(link) fail.On(err != nil, "Client for %q failed, reason: %v", link, err) - if common.TraceFlag { + if common.TraceFlag() { client = client.WithTracing() } request := client.NewRequest("") @@ -156,7 +156,7 @@ func getRequest(link string) (code int, fingerprint string, err error) { client, err := cloud.NewClient(link) fail.On(err != nil, "Client for %q failed, reason: %v", link, err) - if common.TraceFlag { + if common.TraceFlag() { client = client.WithTracing() } request := client.NewRequest("") diff --git a/operations/processtree.go b/operations/processtree.go index bf30508f..675cc81c 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -3,17 +3,19 @@ package operations import ( "fmt" "os" - "sort" "time" "github.com/mitchellh/go-ps" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" ) type ( - ChildMap map[int]string - ProcessMap map[int]*ProcessNode - ProcessNode struct { + ChildMap map[int]string + ProcessMap map[int]*ProcessNode + ProcessNodes []*ProcessNode + ProcessNode struct { Pid int Parent int Executable string @@ -49,12 +51,18 @@ func ProcessMapNow() (ProcessMap, error) { } func (it ProcessMap) Keys() []int { - keys := make([]int, 0, len(it)) - for key, _ := range it { - keys = append(keys, key) + return set.Keys(it) +} + +func (it ProcessMap) Roots() []int { + roots := []int{} + for candidate, node := range it { + _, ok := it[node.Parent] + if !ok { + roots = append(roots, candidate) + } } - sort.Ints(keys) - return keys + return set.Sort(roots) } func (it *ProcessNode) warnings(additional ProcessMap) { @@ -66,8 +74,8 @@ func (it *ProcessNode) warnings(additional ProcessMap) { } if len(additional) > 0 { pretty.Warning("+ migrated process still running:") - for _, zombie := range additional { - zombie.warningTree("| ", true) + for _, key := range additional.Roots() { + additional[key].warningTree("| ", true) } } pretty.Note("Depending on OS, above processes may prevent robot to close properly.") @@ -78,6 +86,7 @@ func (it *ProcessNode) warnings(additional ProcessMap) { pretty.Note("- developer intentionally left processes running, which is not good for repeatable automation") pretty.Highlight("So if you see this message, and robot still seems to be running, it is not!") pretty.Highlight("You now have to take action and stop those processes that are preventing robot to complete.") + pretty.Highlight("Example cleanup command: %s", common.GenerateKillCommand(additional.Keys())) } func (it *ProcessNode) warningTree(prefix string, newparent bool) { @@ -96,28 +105,26 @@ func (it *ProcessNode) warningTree(prefix string, newparent bool) { } func SubprocessWarning(seen ChildMap, use bool) error { + before := len(seen) + if before == 0 { + common.Debug("No tracked subprocesses, which is a good thing.") + return nil + } processes, err := ProcessMapNow() if err != nil { return err } + removeStaleChildren(processes, seen) + after := len(seen) + pretty.DebugNote("Final subprocess count %d -> %d. %v", before, after, seen) + if after == 0 { + common.Debug("No active tracked subprocesses anymore, and that is a good thing.") + return nil + } self, ok := processes[os.Getpid()] if !ok { return fmt.Errorf("For some reason, could not find own process in process map.") } - masked := make(ChildMap) - if use { - for pid, executable := range seen { - ref, ok := processes[pid] - if ok { - updateActiveChildren(ref, masked, 70) - } else { - masked[pid] = executable - } - } - } - for key, _ := range masked { - delete(seen, key) - } additional := make(ProcessMap) for pid, executable := range seen { ref, ok := processes[pid] @@ -140,13 +147,20 @@ func removeStaleChildren(processes ProcessMap, seen ChildMap) { } } -func updateActiveChildren(host *ProcessNode, seen ChildMap, maxDepth int) { - if maxDepth < 0 { - return - } - for pid, child := range host.Children { - seen[pid] = child.Executable - updateActiveChildren(child, seen, maxDepth-1) +func updateActiveChildrenLoop(start *ProcessNode, seen ChildMap) { + counted := make(map[int]bool) + counted[start.Pid] = true + at, todo := 0, ProcessNodes{start} + for at < len(todo) { + for pid, child := range todo[at].Children { + if counted[pid] { + continue + } + seen[pid] = child.Executable + todo = append(todo, child) + counted[pid] = true + } + at += 1 } } @@ -154,7 +168,7 @@ func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) { source, ok := processes[pid] if ok { removeStaleChildren(processes, seen) - updateActiveChildren(source, seen, 70) + updateActiveChildrenLoop(source, seen) } } @@ -167,13 +181,20 @@ func WatchChildren(pid int, delay time.Duration) chan ChildMap { func babySitter(pid int, reply chan ChildMap, delay time.Duration) { defer close(reply) seen := make(ChildMap) - failures := 0 + failures, broadcasted := 0, 0 forever: for failures < 10 { processes, err := ProcessMapNow() if err == nil { updateSeenChildren(pid, processes, seen) failures = 0 + } else { + common.Debug("Process snapshot failure: %v", err) + } + active := len(seen) + if active != broadcasted { + pretty.DebugNote("Active subprocess count %d -> %d. %v", broadcasted, active, seen) + broadcasted = active } select { case reply <- seen: @@ -182,4 +203,5 @@ forever: continue forever } } + common.Debug("Final active subprocess count was %d.", broadcasted) } diff --git a/operations/running.go b/operations/running.go index a7d8fed5..9e5a1f05 100644 --- a/operations/running.go +++ b/operations/running.go @@ -307,7 +307,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if err != nil { pretty.Exit(9, "Error: %v", err) } - if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { + if !flags.NoPipFreeze && !flags.Assistant && !common.Silent() && !interactive { wantedfile, _ := config.DependenciesFile() ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } diff --git a/operations/updownload.go b/operations/updownload.go index 71be4959..0898fc34 100644 --- a/operations/updownload.go +++ b/operations/updownload.go @@ -170,7 +170,7 @@ func SummonRobotZipfile(client cloud.Client, account *account, workspaceId, robo return found, nil } zipfile := filepath.Join(pathlib.TempDir(), fmt.Sprintf("summon%x.zip", common.When)) - err := DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag) + err := DownloadCommand(client, account, workspaceId, robotId, zipfile, common.DebugFlag()) if err != nil { return "", err } diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index 765ec1a5..c940e73a 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -14,7 +14,7 @@ func Locker(filename string, trycount int) (Releaser, error) { if Lockless { return Fake(), nil } - if common.TraceFlag { + if common.TraceFlag() { defer common.Stopwatch("LOCKER: Got lock on %v in", filename).Report() } common.Trace("LOCKER: Want lock on: %v", filename) diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 4eb614f1..53ad1573 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -34,7 +34,7 @@ func Locker(filename string, trycount int) (Releaser, error) { } var file *os.File var err error - if common.TraceFlag { + if common.TraceFlag() { defer func() { common.Stopwatch("LOCKER: Leaving lock on %v with %v retries left in", filename, trycount).Report() }() diff --git a/pretty/functions.go b/pretty/functions.go index 4d37d65b..b4dc5a14 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -11,6 +11,11 @@ func Ok() error { return nil } +func DebugNote(format string, rest ...interface{}) { + niceform := fmt.Sprintf("%s%sNote: %s%s", Blue, Bold, format, Reset) + common.Debug(niceform, rest...) +} + func Note(format string, rest ...interface{}) { niceform := fmt.Sprintf("%s%sNote: %s%s", Cyan, Bold, format, Reset) common.Log(niceform, rest...) diff --git a/pretty/variables.go b/pretty/variables.go index 1e302ad0..938e2952 100644 --- a/pretty/variables.go +++ b/pretty/variables.go @@ -17,6 +17,7 @@ var ( Black string Red string Green string + Blue string Yellow string Magenta string Cyan string @@ -46,9 +47,10 @@ func Setup() { Black = csi("30m") Red = csi("91m") Green = csi("92m") + Yellow = csi("93m") + Blue = csi("94m") Magenta = csi("95m") Cyan = csi("96m") - Yellow = csi("93m") Reset = csi("0m") Home = csi("1;1H") Clear = csi("0J") diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 85275498..9c847e8f 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v14. + Must Have v15. Goal: Show rcc license information. Step build/rcc man license --controller citests From bb2c8384f2f37f8f9438ea422169c4787a738775 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 22 Aug 2023 12:28:13 +0300 Subject: [PATCH 418/516] Environment public cacheability diagnostics (v15.1.0) - robot diagnostics now has indication of environment cacheability and also warnings (category 5010) when something prevents caching - lack of public cacheability is also visible on environment creation - documentation updates and improvements - minor improvements on process tree debugging --- cmd/root.go | 8 ++++---- common/categories.go | 1 + common/version.go | 2 +- conda/condayaml.go | 24 ++++++++++++++++++++++++ docs/changelog.md | 8 ++++++++ docs/recipes.md | 8 ++++++-- htfs/commands.go | 7 ++++++- operations/processtree.go | 30 +++++++++++++++++++++--------- 8 files changed, 71 insertions(+), 17 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 0b6d08e6..e083d0dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -108,16 +108,16 @@ func init() { rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") - rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib") - rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output") + rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib (also RCC_NO_BUILD=1)") + rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output (also RCC_VERBOSITY=silent)") rootCmd.PersistentFlags().BoolVarP(&common.Liveonly, "liveonly", "", false, "do not create base environment from live ... DANGER! For containers only!") rootCmd.PersistentFlags().BoolVarP(&pathlib.Lockless, "lockless", "", false, "do not use file locking ... DANGER!") rootCmd.PersistentFlags().BoolVarP(&pretty.Colorless, "colorless", "", false, "do not use colors in CLI UI") rootCmd.PersistentFlags().BoolVarP(&common.NoCache, "nocache", "", false, "do not use cache for credentials and tokens, always request them from cloud") rootCmd.PersistentFlags().BoolVarP(&common.LogLinenumbers, "numbers", "", false, "put line numbers on rcc produced log output") - rootCmd.PersistentFlags().BoolVarP(&debugFlag, "debug", "", false, "to get debug output where available (not for production use)") - rootCmd.PersistentFlags().BoolVarP(&traceFlag, "trace", "", false, "to get trace output where available (not for production use)") + rootCmd.PersistentFlags().BoolVarP(&debugFlag, "debug", "", false, "to get debug output where available (not for normal production use; also RCC_VERBOSITY=debug)") + rootCmd.PersistentFlags().BoolVarP(&traceFlag, "trace", "", false, "to get trace output where available (not for normal production use; also RCC_VERBOSITY=trace)") rootCmd.PersistentFlags().BoolVarP(&common.TimelineEnabled, "timeline", "", false, "print timeline at the end of run") rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") diff --git a/common/categories.go b/common/categories.go index 4ca94cb6..fd3283fc 100644 --- a/common/categories.go +++ b/common/categories.go @@ -13,4 +13,5 @@ const ( CategoryNetworkLink = 4020 CategoryNetworkHEAD = 4030 CategoryNetworkCanary = 4040 + CategoryEnvironmentCache = 5010 ) diff --git a/common/version.go b/common/version.go index 853cf2e4..7eae5caf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v15.0.0` + Version = `v15.1.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index c3512704..6e13fbf2 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -162,6 +162,20 @@ func SummonEnvironment(filename string) *Environment { } } +func (it *Environment) IsCacheable() bool { + for _, dependency := range it.Conda { + if !dependency.IsCacheable() { + return false + } + } + for _, dependency := range it.Pip { + if !dependency.IsCacheable() { + return false + } + } + return true +} + func (it *Environment) FreezeDependencies(fixed dependencies) *Environment { result := &Environment{ Name: it.Name, @@ -525,6 +539,8 @@ func (it *Environment) FingerprintLayers() [3]string { } func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production bool) { + target.Details["cacheable-environment-configuration"] = fmt.Sprintf("%v", it.IsCacheable()) + diagnose := target.Diagnose("Conda") notice := diagnose.Warning if production { @@ -560,6 +576,10 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b notice(0, "", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) } packages[presentation] = true + if !dependency.IsCacheable() { + diagnose.Warning(common.CategoryEnvironmentCache, "", "Conda dependency %q is not publicly cacheable.", dependency.Original) + ok = false + } if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { notice(0, "", "Floating conda dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false @@ -581,6 +601,10 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b notice(0, "", "Dependency %q seems to be duplicate of previous dependency.", dependency.Original) } packages[presentation] = true + if !dependency.IsCacheable() { + diagnose.Warning(common.CategoryEnvironmentCache, "", "Pip dependency %q is not publicly cacheable.", dependency.Original) + ok = false + } if strings.Contains(dependency.Versions, "*") || len(dependency.Qualifier) == 0 || len(dependency.Versions) == 0 { notice(0, "", "Floating pip dependency %q should be bound to exact version before taking robot into production.", dependency.Original) ok = false diff --git a/docs/changelog.md b/docs/changelog.md index df331e9e..f984dbfd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v15.1.0 (date: 22.8.2023) + +- robot diagnostics now has indication of environment cacheability and also + warnings (category 5010) when something prevents caching +- lack of public cacheability is also visible on environment creation +- documentation updates and improvements +- minor improvements on process tree debugging + ## v15.0.0 (date: 21.8.2023) WORK IN PROGRESS - breaking change: dropped default value `rcc robot initialize --template` diff --git a/docs/recipes.md b/docs/recipes.md index eb45d5a5..dcfc949e 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -59,8 +59,9 @@ This is how you can experiment with it. something like `environment_xxx_yyy_freeze.yaml` - copy that file back into your robot, right beside existing `conda.yaml` file (but do not overwrite it, you need that later) -- edit your `robot.yaml` file to point `condaConfigFile` entry to your - newly created `environment_xxx_yyy_freeze.yaml` file +- edit your `robot.yaml` file at `condaConfigFile` entry, and add your + newly copied `environment_xxx_yyy_freeze.yaml` file there if it does not + already exist there - repackage your robot and now your environment should stay quite frozen ### Limitations @@ -453,6 +454,9 @@ rcc holotree init --revoke - `RCC_NO_BUILD` with any non-empty value will prevent rcc for creating new environments (also available as `--no-build` CLI flag, and as an option in `settings.yaml` file) +- `RCC_VERBOSITY` controls how verbose rcc output will be. If this variable + is not set, then verbosity is taken from `--silent`, `--debug`, and `--trace` + CLI flags. Valid values for this variable are `silent`, `debug` and `trace`. ## How to troubleshoot rcc setup and robots? diff --git a/htfs/commands.go b/htfs/commands.go index cdb80758..19411d10 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -242,5 +242,10 @@ func ComposeFinalBlueprint(userFiles []string, packfile string) (config robot.Ro fail.On(right == nil, "Missing environment specification(s).") content, err := right.AsYaml() fail.On(err != nil, "YAML error: %v", err) - return config, []byte(strings.TrimSpace(content)), nil + blueprint = []byte(strings.TrimSpace(content)) + if !right.IsCacheable() { + fingerprint := common.BlueprintHash(blueprint) + pretty.Warning("Holotree blueprint %q is not publicly cacheable. Use `rcc robot diagnostics` to find out more.", fingerprint) + } + return config, blueprint, nil } diff --git a/operations/processtree.go b/operations/processtree.go index 675cc81c..3e09f0c2 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -138,16 +138,20 @@ func SubprocessWarning(seen ChildMap, use bool) error { return nil } -func removeStaleChildren(processes ProcessMap, seen ChildMap) { +func removeStaleChildren(processes ProcessMap, seen ChildMap) bool { + removed := false for key, name := range seen { found, ok := processes[key] if !ok || found.Executable != name { delete(seen, key) + removed = true } } + return removed } -func updateActiveChildrenLoop(start *ProcessNode, seen ChildMap) { +func updateActiveChildrenLoop(start *ProcessNode, seen ChildMap) bool { + updated := false counted := make(map[int]bool) counted[start.Pid] = true at, todo := 0, ProcessNodes{start} @@ -156,20 +160,27 @@ func updateActiveChildrenLoop(start *ProcessNode, seen ChildMap) { if counted[pid] { continue } + counted[pid] = true + _, previously := seen[pid] seen[pid] = child.Executable todo = append(todo, child) - counted[pid] = true + if !previously { + updated = true + } } at += 1 } + return updated } -func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) { +func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) bool { source, ok := processes[pid] if ok { - removeStaleChildren(processes, seen) - updateActiveChildrenLoop(source, seen) + removed := removeStaleChildren(processes, seen) + updated := updateActiveChildrenLoop(source, seen) + return removed || updated } + return false } func WatchChildren(pid int, delay time.Duration) chan ChildMap { @@ -184,15 +195,16 @@ func babySitter(pid int, reply chan ChildMap, delay time.Duration) { failures, broadcasted := 0, 0 forever: for failures < 10 { + updated := false processes, err := ProcessMapNow() if err == nil { - updateSeenChildren(pid, processes, seen) + updated = updateSeenChildren(pid, processes, seen) failures = 0 } else { common.Debug("Process snapshot failure: %v", err) } - active := len(seen) - if active != broadcasted { + if updated { + active := len(seen) pretty.DebugNote("Active subprocess count %d -> %d. %v", broadcasted, active, seen) broadcasted = active } From 7ef9f37f373abc69ddb9144767a664f22be0fc06 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 23 Aug 2023 11:11:58 +0300 Subject: [PATCH 419/516] Micromamba management by version numbers (v15.2.0) - new strategy to manage micromamba, with its own directory based on version number: `ROBOCORP_HOME/micromamba//` - updated cleanup to manage micromamba location change - bugfix: speedtest now does timing also in debug/trace mode (and some other minor improvements) --- cmd/speed.go | 43 +++++++++++++++++--------------------- common/variables.go | 4 ++++ common/version.go | 2 +- conda/cleanup.go | 9 ++++++-- conda/platform_darwin.go | 9 +++++++- conda/platform_linux.go | 9 +++++++- conda/platform_windows.go | 8 ++++++- docs/changelog.md | 8 +++++++ robot_tests/holotree.robot | 2 +- 9 files changed, 63 insertions(+), 31 deletions(-) diff --git a/cmd/speed.go b/cmd/speed.go index ec5dcacd..99cf7a7b 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -26,13 +26,17 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) } -func workingWorm(pipe chan bool, reply chan int) { - fmt.Fprintf(os.Stderr, "\nWorking: -----") +func workingWorm(pipe chan bool, reply chan int, debug bool) { + if !debug { + fmt.Fprintf(os.Stderr, "\nWorking: -----") + } seconds := 0 loop: for { - fmt.Fprintf(os.Stderr, "\b\b\b\b\b%4ds", seconds) - os.Stderr.Sync() + if !debug { + fmt.Fprintf(os.Stderr, "\b\b\b\b\b%4ds", seconds) + os.Stderr.Sync() + } select { case <-time.After(1 * time.Second): seconds += 1 @@ -57,40 +61,31 @@ var speedtestCmd = &cobra.Command{ common.Log("This may take several minutes, please be patient.") signal := make(chan bool) timing := make(chan int) - silent, trace, debug := common.Silent(), common.TraceFlag(), common.DebugFlag() + silent, debug, trace := common.Silent(), common.DebugFlag(), common.TraceFlag() if !debug { - go workingWorm(signal, timing) common.DefineVerbosity(true, false, false) } + go workingWorm(signal, timing, debug) folder := common.RobocorpTemp() + pretty.DebugNote("Speed test will force temporary ROBOCORP_HOME to be %q while testing.", folder) + err := os.RemoveAll(folder) + pretty.Guard(err == nil, 4, "Error: %v", err) content, err := blobs.Asset("assets/speedtest.yaml") - if err != nil { - pretty.Exit(1, "Error: %v", err) - } + pretty.Guard(err == nil, 1, "Error: %v", err) condafile := filepath.Join(folder, "speedtest.yaml") err = pathlib.WriteFile(condafile, content, 0o666) - if err != nil { - pretty.Exit(2, "Error: %v", err) - } + pretty.Guard(err == nil, 2, "Error: %v", err) common.ForcedRobocorpHome = folder _, score, err := htfs.NewEnvironment(condafile, "", true, true, operations.PullCatalog) common.DefineVerbosity(silent, debug, trace) - if err != nil { - pretty.Exit(3, "Error: %v", err) - } + pretty.Guard(err == nil, 3, "Error: %v", err) common.ForcedRobocorpHome = "" err = os.RemoveAll(folder) - if err != nil { - pretty.Exit(4, "Error: %v", err) - } + pretty.Guard(err == nil, 4, "Error: %v", err) score.Done() close(signal) - if !debug { - elapsed := <-timing - common.Log("%s", score.Score(anywork.Scale(), elapsed)) - } else { - common.Log("%s", score.Score(anywork.Scale(), 0.0)) - } + elapsed := <-timing + common.Log("%s", score.Score(anywork.Scale(), elapsed)) pretty.Ok() }, } diff --git a/common/variables.go b/common/variables.go index 11072aa8..2f826cd3 100644 --- a/common/variables.go +++ b/common/variables.go @@ -175,6 +175,10 @@ func BinLocation() string { return filepath.Join(RobocorpHome(), "bin") } +func MicromambaLocation() string { + return filepath.Join(RobocorpHome(), "micromamba") +} + func SharedMarkerLocation() string { return filepath.Join(HoloLocation(), "shared.yes") } diff --git a/common/version.go b/common/version.go index 7eae5caf..187beeaa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v15.1.0` + Version = `v15.2.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index cb029d1f..7eaaf79c 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -101,7 +101,8 @@ func spotlessCleanup(dryrun bool) error { } rcccache := filepath.Join(common.RobocorpHome(), "rcccache.yaml") if dryrun { - common.Log("- %v", BinMicromamba()) + common.Log("- %v", common.BinLocation()) + common.Log("- %v", common.MicromambaLocation()) common.Log("- %v", common.RobotCache()) common.Log("- %v", rcccache) common.Log("- %v", common.OldEventJournal()) @@ -110,7 +111,8 @@ func spotlessCleanup(dryrun bool) error { common.Log("- %v", common.HololibLocation()) return nil } - safeRemove("executable", BinMicromamba()) + safeRemove("executables", common.BinLocation()) + safeRemove("micromamba", common.MicromambaLocation()) safeRemove("cache", common.RobotCache()) safeRemove("cache", rcccache) safeRemove("old", common.OldEventJournal()) @@ -191,6 +193,9 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba, downloads bool) error if micromamba && err == nil { err = doCleanup(BinMicromamba(), dryrun) } + if micromamba && err == nil { + err = doCleanup(common.MicromambaLocation(), dryrun) + } return err } diff --git a/conda/platform_darwin.go b/conda/platform_darwin.go index d1fbc872..d27d1b57 100644 --- a/conda/platform_darwin.go +++ b/conda/platform_darwin.go @@ -6,6 +6,8 @@ import ( "path/filepath" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" ) @@ -36,7 +38,12 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba")) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + err := pathlib.EnsureDirectoryExists(location) + if err != nil { + pretty.Warning("Problem creating %q, reason: %v", location, err) + } + return common.ExpandPath(filepath.Join(location, "micromamba")) } func CondaPaths(prefix string) []string { diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 55d35baa..8d99ba80 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -6,6 +6,8 @@ import ( "path/filepath" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" ) @@ -40,7 +42,12 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba")) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + err := pathlib.EnsureDirectoryExists(location) + if err != nil { + pretty.Warning("Problem creating %q, reason: %v", location, err) + } + return common.ExpandPath(filepath.Join(location, "micromamba")) } func CondaPaths(prefix string) []string { diff --git a/conda/platform_windows.go b/conda/platform_windows.go index d1011e80..5eeb8b91 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -8,6 +8,7 @@ import ( "golang.org/x/sys/windows/registry" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" "github.com/robocorp/rcc/shell" @@ -46,7 +47,12 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - return common.ExpandPath(filepath.Join(common.BinLocation(), "micromamba.exe")) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + err := pathlib.EnsureDirectoryExists(location) + if err != nil { + pretty.Warning("Problem creating %q, reason: %v", location, err) + } + return common.ExpandPath(filepath.Join(location, "micromamba.exe")) } func CondaPaths(prefix string) []string { diff --git a/docs/changelog.md b/docs/changelog.md index f984dbfd..de500c9a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v15.2.0 (date: 23.8.2023) + +- new strategy to manage micromamba, with its own directory based on version + number: `ROBOCORP_HOME/micromamba//` +- updated cleanup to manage micromamba location change +- bugfix: speedtest now does timing also in debug/trace mode (and some other + minor improvements) + ## v15.1.0 (date: 22.8.2023) - robot diagnostics now has indication of environment cacheability and also diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 67db9b73..42f864c9 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -125,7 +125,7 @@ Goal: Liveonly works and uses virtual holotree Goal: Do quick cleanup on environments Step build/rcc config cleanup --controller citests --quick - Must Exist %{ROBOCORP_HOME}/bin/micromamba + Must Exist %{ROBOCORP_HOME}/micromamba/ Wont Exist %{ROBOCORP_HOME}/pkgs/ Wont Exist %{ROBOCORP_HOME}/pipcache/ Wont Exist %{ROBOCORP_HOME}/templates/ From eeed56a9f68524d0e8d157e0a05c63992efef246 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 30 Aug 2023 13:56:09 +0300 Subject: [PATCH 420/516] Run journal added (v15.3.0) - added `journal.run` event log into artifacts directory - tidying some golang dependencies and removing some unused files --- cmd/run.go | 1 + common/journal.go | 22 ++++++++++++++++ common/logger.go | 1 + common/version.go | 2 +- conda/cacheable.go | 1 - docs/changelog.md | 5 ++++ go.mod | 4 +-- journal/journal.go | 55 ++++++++++++++++++++++++++++++++++----- operations/diagnostics.go | 1 + operations/processtree.go | 3 +++ operations/running.go | 10 +++++++ pathlib/validators.go | 8 ++++-- 12 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 common/journal.go delete mode 100644 conda/cacheable.go diff --git a/cmd/run.go b/cmd/run.go index 92977b2f..b2182005 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -25,6 +25,7 @@ in your own machine.`, Run: func(cmd *cobra.Command, args []string) { defer conda.RemoveCurrentTemp() defer journal.BuildEventStats("robot") + defer journal.StopRunJournal() if common.DebugFlag() { defer common.Stopwatch("Task run lasted").Report() } diff --git a/common/journal.go b/common/journal.go new file mode 100644 index 00000000..964e3832 --- /dev/null +++ b/common/journal.go @@ -0,0 +1,22 @@ +package common + +var ( + journal runJournal +) + +type ( + runJournal interface { + Post(string, string, string, ...interface{}) error + } +) + +func RegisterJournal(target runJournal) { + journal = target +} + +func RunJournal(event, detail, commentForm string, fields ...interface{}) error { + if journal != nil { + return journal.Post(event, detail, commentForm, fields...) + } + return nil +} diff --git a/common/logger.go b/common/logger.go index 8cb137a0..93ee2518 100644 --- a/common/logger.go +++ b/common/logger.go @@ -106,4 +106,5 @@ func Progress(step int, form string, details ...interface{}) { message := fmt.Sprintf(form, details...) Log("#### Progress: %02d/15 %s %8.3fs %s", step, Version, delta, message) Timeline("%d/15 %s", step, message) + RunJournal("environment", "build", "Progress: %02d/15 %s %8.3fs %s", step, Version, delta, message) } diff --git a/common/version.go b/common/version.go index 187beeaa..fb169fc1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v15.2.0` + Version = `v15.3.0` ) diff --git a/conda/cacheable.go b/conda/cacheable.go deleted file mode 100644 index e38064af..00000000 --- a/conda/cacheable.go +++ /dev/null @@ -1 +0,0 @@ -package conda diff --git a/docs/changelog.md b/docs/changelog.md index de500c9a..5e8bce9f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v15.3.0 (date: 30.8.2023) + +- added `journal.run` event log into artifacts directory +- tidying some golang dependencies and removing some unused files + ## v15.2.0 (date: 23.8.2023) - new strategy to manage micromamba, with its own directory based on version diff --git a/go.mod b/go.mod index f9c7a983..9e53d692 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/robocorp/rcc -go 1.18 +go 1.20 require ( github.com/dchest/siphash v1.2.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.14 + github.com/mitchellh/go-ps v1.0.0 github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.13.0 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f @@ -19,7 +20,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect - github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect diff --git a/journal/journal.go b/journal/journal.go index 89e264ca..dc0ec16b 100644 --- a/journal/journal.go +++ b/journal/journal.go @@ -8,6 +8,8 @@ import ( "os" "regexp" "strings" + "sync" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" @@ -15,14 +17,55 @@ import ( var ( spacePattern = regexp.MustCompile("\\s+") + runJournal *journal ) -type Event struct { - When int64 `json:"when"` - Controller string `json:"controller"` - Event string `json:"event"` - Detail string `json:"detail"` - Comment string `json:"comment,omitempty"` +type ( + journal struct { + sync.Mutex + filename string + } + Event struct { + When int64 `json:"when"` + Controller string `json:"controller"` + Event string `json:"event"` + Detail string `json:"detail"` + Comment string `json:"comment,omitempty"` + } +) + +func init() { + runJournal = &journal{sync.Mutex{}, ""} + common.RegisterJournal(runJournal) +} + +func ForRun(filename string) { + runJournal.Lock() + defer runJournal.Unlock() + runJournal.filename = filename +} + +func StopRunJournal() { + ForRun("") +} + +func (it *journal) Post(event, detail, commentForm string, fields ...interface{}) (err error) { + if it == nil || len(it.filename) == 0 { + return nil + } + defer fail.Around(&err) + it.Lock() + defer it.Unlock() + message := Event{ + When: time.Now().Unix(), + Controller: common.ControllerIdentity(), + Event: Unify(event), + Detail: detail, + Comment: Unify(fmt.Sprintf(commentForm, fields...)), + } + blob, err := json.Marshal(message) + fail.On(err != nil, "Could not serialize event: %v -> %v", event, err) + return appendJournal(it.filename, blob) } func Unify(value string) string { diff --git a/operations/diagnostics.go b/operations/diagnostics.go index ee08785d..7947b274 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -98,6 +98,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["os"] = common.Platform() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") + result.Details["timezone"] = time.Now().Format("MST") result.Details["no-build"] = fmt.Sprintf("%v", settings.Global.NoBuild()) result.Details["ENV:ComSpec"] = os.Getenv("ComSpec") result.Details["ENV:SHELL"] = os.Getenv("SHELL") diff --git a/operations/processtree.go b/operations/processtree.go index 3e09f0c2..affdb163 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -98,6 +98,7 @@ func (it *ProcessNode) warningTree(prefix string, newparent bool) { kind = fmt.Sprintf("%s -> new parent PID: #%d", kind, it.Parent) } pretty.Warning("%s#%d %q <%s>", prefix, it.Pid, it.Executable, kind) + common.RunJournal("orphan process", fmt.Sprintf("parent=%d pid=%d name=%s", it.Parent, it.Pid, it.Executable), "process pollution") indent := prefix + "| " for _, key := range it.Children.Keys() { it.Children[key].warningTree(indent, false) @@ -193,6 +194,7 @@ func babySitter(pid int, reply chan ChildMap, delay time.Duration) { defer close(reply) seen := make(ChildMap) failures, broadcasted := 0, 0 + defer common.RunJournal("processes", "final", "count: %d", broadcasted) forever: for failures < 10 { updated := false @@ -206,6 +208,7 @@ forever: if updated { active := len(seen) pretty.DebugNote("Active subprocess count %d -> %d. %v", broadcasted, active, seen) + common.RunJournal("processes", "updated", "count from %d to %d ... %v", broadcasted, active, seen) broadcasted = active } select { diff --git a/operations/running.go b/operations/running.go index 9e5a1f05..d85b79c7 100644 --- a/operations/running.go +++ b/operations/running.go @@ -173,6 +173,9 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. pretty.Exit(4, "Error: this robot requires holotree, but no --space was given!") } + journal.ForRun(filepath.Join(config.ArtifactDirectory(), "journal.run")) + common.RunJournal("start task", fmt.Sprintf("name=%s from=%s", theTask, packfile), "at task environment setup") + if !config.UsesConda() { return true, config, todo, "" } @@ -186,12 +189,16 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { common.TimelineBegin("robot execution (simple=%v).", simple) + common.RunJournal("start", "robot", "started") + defer common.RunJournal("stop", "robot", "done") defer common.TimelineEnd() pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) if simple { + common.RunJournal("select", "robot", "simple run") pathlib.NoteDirectoryContent("[Before run] Artifact dir", config.ArtifactDirectory(), true) ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { + common.RunJournal("run", "robot", "task run") ExecuteTask(runFlags, template, config, todo, label, interactive, extraEnv) } } @@ -367,12 +374,15 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro func showRccPointOfView(err error) { printer := pretty.Lowlight message := fmt.Sprintf("@@@ %s SUCCESS. @@@", rccpov) + journal := fmt.Sprintf("%s SUCCESS.", rccpov) if err != nil { printer = pretty.Highlight message = fmt.Sprintf("@@@ %s FAILURE, reason: %q. See details above. @@@", rccpov, err) + journal = fmt.Sprintf("%s FAILURE, reason: %s", rccpov, err) } banner := strings.Repeat("@", len(message)) printer(banner) printer(message) printer(banner) + common.RunJournal("robot exit", journal, "rcc point of view") } diff --git a/pathlib/validators.go b/pathlib/validators.go index 5ff64730..f9f8929e 100644 --- a/pathlib/validators.go +++ b/pathlib/validators.go @@ -47,10 +47,14 @@ func NoteDirectoryContent(context, directory string, guide bool) { if err != nil { return } + noted := false for _, entry := range entries { - pretty.Note("%s %q already has %q in it.", context, fullpath, entry.Name()) + if entry.Name() != "journal.run" { + pretty.Note("%s %q already has %q in it.", context, fullpath, entry.Name()) + noted = true + } } - if guide && len(entries) > 0 { + if guide && noted { pretty.Highlight("Above notes mean, that there were files present in directory that was supposed to be empty!") pretty.Highlight("In robot development phase, it might be ok to have these files while building robot.") pretty.Highlight("In production robot/assistant, this might be a mistake, where development files were") From b2fcb8b0f056d8b021f2ed6b866896d59026a444 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Sep 2023 07:29:56 +0300 Subject: [PATCH 421/516] Breaking change: TLS diagnostics (v16.0.0) - Breaking change: there is new TLS verification in place in diagnostic, and this can break some old setups because new warnings. --- common/categories.go | 2 + common/version.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 5 ++ operations/diagnostics.go | 5 ++ operations/tlscheck.go | 115 ++++++++++++++++++++++++++++++++++++++ robot_tests/fullrun.robot | 2 +- settings/settings.go | 2 +- 8 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 operations/tlscheck.go diff --git a/common/categories.go b/common/categories.go index fd3283fc..5d60137d 100644 --- a/common/categories.go +++ b/common/categories.go @@ -13,5 +13,7 @@ const ( CategoryNetworkLink = 4020 CategoryNetworkHEAD = 4030 CategoryNetworkCanary = 4040 + CategoryNetworkTLSVersion = 4050 + CategoryNetworkTLSVerify = 4060 CategoryEnvironmentCache = 5010 ) diff --git a/common/version.go b/common/version.go index fb169fc1..d0d78283 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v15.3.0` + Version = `v16.0.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index e35c25af..dd4c57db 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -150,7 +150,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if settings.Global.NoRevocation() { environment = append(environment, "MAMBA_SSL_NO_REVOKE=true") } - if settings.Global.VerifySsl() { + if !settings.Global.VerifySsl() { environment = append(environment, "MAMBA_SSL_VERIFY=false") } environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) diff --git a/docs/changelog.md b/docs/changelog.md index 5e8bce9f..1a251312 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.0.0 (date: 5.9.2023) WORK IN PROGRESS + +- Breaking change: there is new TLS verification in place in diagnostic, and + this can break some old setups because new warnings. + ## v15.3.0 (date: 30.8.2023) - added `journal.run` event log into artifacts directory diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 7947b274..06d7e399 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -150,6 +150,11 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Checks = append(result.Checks, dnsLookupCheck(host)) } result.Details["dns-lookup-time"] = dnsStopwatch.Text() + tlsStopwatch := common.Stopwatch("TLS verification time for %d hostnames was about", len(hostnames)) + for _, host := range hostnames { + result.Checks = append(result.Checks, tlsCheckHost(host)...) + } + result.Details["tls-lookup-time"] = tlsStopwatch.Text() result.Checks = append(result.Checks, canaryDownloadCheck()) result.Checks = append(result.Checks, pypiHeadCheck()) result.Checks = append(result.Checks, condaHeadCheck()) diff --git a/operations/tlscheck.go b/operations/tlscheck.go new file mode 100644 index 00000000..f2a941b7 --- /dev/null +++ b/operations/tlscheck.go @@ -0,0 +1,115 @@ +package operations + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/settings" +) + +var ( + tlsVersions = map[uint16]string{} +) + +func init() { + tlsVersions[tls.VersionSSL30] = "SSLv3" + tlsVersions[tls.VersionTLS10] = "TLS 1.0" + tlsVersions[tls.VersionTLS11] = "TLS 1.1" + tlsVersions[tls.VersionTLS12] = "TLS 1.2" + tlsVersions[tls.VersionTLS13] = "TLS 1.3" +} + +func get(url string) (*tls.ConnectionState, error) { + transport := settings.Global.ConfiguredHttpTransport() + transport.TLSClientConfig.InsecureSkipVerify = true + client := http.Client{Transport: transport} + response, err := client.Head(url) + if err != nil { + return nil, err + } + return response.TLS, nil +} + +func tlsCheckHost(host string) []*common.DiagnosticCheck { + transport := settings.Global.ConfiguredHttpTransport() + result := []*common.DiagnosticCheck{} + supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") + url := fmt.Sprintf("https://%s/", host) + state, err := get(url) + if err != nil { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkLink, + Status: statusWarning, + Message: fmt.Sprintf("%s -> %v", url, err), + Link: supportNetworkUrl, + }) + return result + } + server := state.ServerName + version, ok := tlsVersions[state.Version] + if !ok { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSVersion, + Status: statusWarning, + Message: fmt.Sprintf("unknown TLS version: %q -> %03x", host, state.Version), + Link: supportNetworkUrl, + }) + } else { + tlsStatus := statusOk + if state.Version < tls.VersionTLS12 { + tlsStatus = statusWarning + } + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSVersion, + Status: tlsStatus, + Message: fmt.Sprintf("TLS version: %q -> %s", host, version), + Link: supportNetworkUrl, + }) + } + toVerify := x509.VerifyOptions{ + DNSName: server, + Roots: transport.TLSClientConfig.RootCAs, + Intermediates: x509.NewCertPool(), + } + certificates := state.PeerCertificates + if len(certificates) == 0 { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSVerify, + Status: statusWarning, + Message: fmt.Sprintf("no certificates for %s", server), + Link: supportNetworkUrl, + }) + return result + } + last := certificates[0] + for _, certificate := range certificates[1:] { + toVerify.Intermediates.AddCert(certificate) + last = certificate + } + _, err = certificates[0].Verify(toVerify) + if err != nil { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSVerify, + Status: statusWarning, + Message: fmt.Sprintf("TLS verification of %q failed, reason: %v [last issuer: %q]", server, err, last.Issuer), + Link: supportNetworkUrl, + }) + } else { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSVerify, + Status: statusOk, + Message: fmt.Sprintf("TLS verification of %q passed with certificate issued by %q", server, last.Issuer), + Link: supportNetworkUrl, + }) + } + return result +} diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 9c847e8f..a3afb032 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v15. + Must Have v16. Goal: Show rcc license information. Step build/rcc man license --controller citests diff --git a/settings/settings.go b/settings/settings.go index f9edd7b9..27908e4f 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -245,7 +245,7 @@ func (it gateway) NoBuild() bool { } func (it gateway) ConfiguredHttpTransport() *http.Transport { - return httpTransport + return httpTransport.Clone() } func (it gateway) loadRootCAs() *x509.CertPool { From d997d91ebf5d51f145f3e944849f944ef9ad5924 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Sep 2023 09:47:41 +0300 Subject: [PATCH 422/516] Breaking change: TLS diagnostics continued (v16.0.1) --- common/categories.go | 1 + common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/tlscheck.go | 20 ++++++++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/common/categories.go b/common/categories.go index 5d60137d..1e60a6b7 100644 --- a/common/categories.go +++ b/common/categories.go @@ -15,5 +15,6 @@ const ( CategoryNetworkCanary = 4040 CategoryNetworkTLSVersion = 4050 CategoryNetworkTLSVerify = 4060 + CategoryNetworkTLSChain = 4070 CategoryEnvironmentCache = 5010 ) diff --git a/common/version.go b/common/version.go index d0d78283..9d525773 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.0.0` + Version = `v16.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1a251312..f5fb79eb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.0.1 (date: 5.9.2023) WORK IN PROGRESS + +- Added full signature chain "dump" in case where there is some kind of + certificate failure in TLS verification. Network diagnostics still. + ## v16.0.0 (date: 5.9.2023) WORK IN PROGRESS - Breaking change: there is new TLS verification in place in diagnostic, and diff --git a/operations/tlscheck.go b/operations/tlscheck.go index f2a941b7..84afcac4 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "fmt" "net/http" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/settings" @@ -33,6 +34,18 @@ func get(url string) (*tls.ConnectionState, error) { return response.TLS, nil } +func certificateChain(certificates []*x509.Certificate) string { + parts := make([]string, 0, len(certificates)) + for at, certificate := range certificates { + names := strings.Join(certificate.DNSNames, ", ") + before := certificate.NotBefore.Format("2006-Jan-02") + after := certificate.NotAfter.Format("2006-Jan-02") + form := fmt.Sprintf("#%d: [% 02X ...] names [%s] %s...%s %q issued by %q", at, certificate.Signature[:6], names, before, after, certificate.Subject, certificate.Issuer) + parts = append(parts, form) + } + return strings.Join(parts, "; ") +} + func tlsCheckHost(host string) []*common.DiagnosticCheck { transport := settings.Global.ConfiguredHttpTransport() result := []*common.DiagnosticCheck{} @@ -102,6 +115,13 @@ func tlsCheckHost(host string) []*common.DiagnosticCheck { Message: fmt.Sprintf("TLS verification of %q failed, reason: %v [last issuer: %q]", server, err, last.Issuer), Link: supportNetworkUrl, }) + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSChain, + Status: statusWarning, + Message: fmt.Sprintf("%q certificate chain is {%s}.", host, certificateChain(certificates)), + Link: supportNetworkUrl, + }) } else { result = append(result, &common.DiagnosticCheck{ Type: "network", From f8bd90f019ceb13a577304d77aab7bf2d738568e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Sep 2023 11:23:43 +0300 Subject: [PATCH 423/516] Breaking change: advanced TLS diagnostics (v16.1.0) - Now advanced network diagnostics also have separate `tls-verify` configuration to enable TLS verifications from custom addresses. --- assets/netdiag.yaml | 2 ++ common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/netdiagnostics.go | 9 ++++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/assets/netdiag.yaml b/assets/netdiag.yaml index d3838931..ad9aff6f 100644 --- a/assets/netdiag.yaml +++ b/assets/netdiag.yaml @@ -1,6 +1,8 @@ network: dns-lookup: - www.robocorp.com + tls-verify: + - chat.robocorp.com head-request: - url: https://www.robocorp.com codes: [200] diff --git a/common/version.go b/common/version.go index 9d525773..b0f03d0d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.0.1` + Version = `v16.1.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index f5fb79eb..de189ebd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.1.0 (date: 5.9.2023) WORK IN PROGRESS + +- Now advanced network diagnostics also have separate `tls-verify` configuration + to enable TLS verifications from custom addresses. + ## v16.0.1 (date: 5.9.2023) WORK IN PROGRESS - Added full signature chain "dump" in case where there is some kind of diff --git a/operations/netdiagnostics.go b/operations/netdiagnostics.go index 001c3da7..f984c772 100644 --- a/operations/netdiagnostics.go +++ b/operations/netdiagnostics.go @@ -23,6 +23,7 @@ type ( NetConfig struct { DNS []string `yaml:"dns-lookup"` + TLS []string `yaml:"tls-verify"` Head []*WebConfig `yaml:"head-request"` Get []*WebConfig `yaml:"get-request"` } @@ -35,8 +36,9 @@ type ( ) func (it *NetConfig) Hostnames() []string { - result := make([]string, 0, len(it.DNS)) + result := make([]string, 0, len(it.DNS)+len(it.TLS)) result = append(result, it.DNS...) + result = append(result, it.TLS...) for _, entry := range it.Head { parsed, err := url.Parse(entry.URL) if err == nil { @@ -73,6 +75,11 @@ func networkDiagnostics(config *Configuration, target *common.DiagnosticStatus) target.Checks = append(target.Checks, dnsLookupCheck(host)) } target.Details["dns-lookup-time"] = dnsStopwatch.Text() + tlsStopwatch := common.Stopwatch("TLS verification time for %d hostnames was about", len(hostnames)) + for _, host := range hostnames { + target.Checks = append(target.Checks, tlsCheckHost(host)...) + } + target.Details["tls-lookup-time"] = tlsStopwatch.Text() headStopwatch := common.Stopwatch("HEAD request time for %d requests was about", len(config.Network.Head)) for _, entry := range config.Network.Head { target.Checks = append(target.Checks, webDiagnostics("HEAD", common.CategoryNetworkHEAD, headRequest, entry, supportUrl)...) From 80a5ca6ea45b81edae5df551599b2c38867a5ab0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 5 Sep 2023 13:22:03 +0300 Subject: [PATCH 424/516] Bug fix: micromamba proxy settings (v16.1.1) - bug fix: added missing proxies to micromamba phase --- common/version.go | 2 +- conda/platform_darwin.go | 2 +- conda/platform_linux.go | 2 +- conda/platform_windows.go | 2 +- conda/robocorp.go | 25 +++++++++++++++---------- docs/changelog.md | 4 ++++ 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/common/version.go b/common/version.go index b0f03d0d..16e10935 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.1.0` + Version = `v16.1.1` ) diff --git a/conda/platform_darwin.go b/conda/platform_darwin.go index d27d1b57..83e79207 100644 --- a/conda/platform_darwin.go +++ b/conda/platform_darwin.go @@ -34,7 +34,7 @@ func CondaEnvironment() []string { tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) - return env + return injectNetworkEnvironment(env) } func BinMicromamba() string { diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 8d99ba80..6db517ec 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -38,7 +38,7 @@ func CondaEnvironment() []string { tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) - return env + return injectNetworkEnvironment(env) } func BinMicromamba() string { diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 5eeb8b91..77309a8c 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -43,7 +43,7 @@ func CondaEnvironment() []string { tempFolder := common.RobocorpTemp() env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) - return env + return injectNetworkEnvironment(env) } func BinMicromamba() string { diff --git a/conda/robocorp.go b/conda/robocorp.go index dd4c57db..c950ae11 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -108,6 +108,20 @@ func FindPython(location string) (string, bool) { return holotreePath.Which("python", FileExtensions) } +func injectNetworkEnvironment(environment []string) []string { + if settings.Global.NoRevocation() { + environment = append(environment, "MAMBA_SSL_NO_REVOKE=true") + } + if !settings.Global.VerifySsl() { + environment = append(environment, "MAMBA_SSL_VERIFY=false") + } + environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) + environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) + environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) + environment = appendIfValue(environment, "HTTP_PROXY", settings.Global.HttpProxy()) + return environment +} + func CondaExecutionEnvironment(location string, inject []string, full bool) []string { environment := make([]string, 0, 100) if full { @@ -147,16 +161,7 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st FindPath(location).AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) - if settings.Global.NoRevocation() { - environment = append(environment, "MAMBA_SSL_NO_REVOKE=true") - } - if !settings.Global.VerifySsl() { - environment = append(environment, "MAMBA_SSL_VERIFY=false") - } - environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) - environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) - environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) - environment = appendIfValue(environment, "HTTP_PROXY", settings.Global.HttpProxy()) + environment = injectNetworkEnvironment(environment) if settings.Global.HasPipRc() { environment = appendIfValue(environment, "PIP_CONFIG_FILE", common.PipRcFile()) } diff --git a/docs/changelog.md b/docs/changelog.md index de189ebd..6f0fa5ae 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v16.1.1 (date: 5.9.2023) WORK IN PROGRESS + +- bug fix: added missing proxies to micromamba phase + ## v16.1.0 (date: 5.9.2023) WORK IN PROGRESS - Now advanced network diagnostics also have separate `tls-verify` configuration From 194325e02482b0f76d5c008f2876d328338d5d05 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 6 Sep 2023 08:15:10 +0300 Subject: [PATCH 425/516] Bugfix: allowed TLS versions (v16.1.2) - bug fix: allowing detection of lower levels of TLS versions - minor improvement: diagnostics TLS firewall/proxy detection - minor improvement: full certificate chain is now behind `--debug` flag --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 10 +++++++++- operations/netdiagnostics.go | 10 +++++++++- operations/tlscheck.go | 20 ++++++++++++-------- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/common/version.go b/common/version.go index 16e10935..71a04096 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.1.1` + Version = `v16.1.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 6f0fa5ae..33fc4072 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v16.1.2 (date: 6.9.2023) WORK IN PROGRESS + +- bug fix: allowing detection of lower levels of TLS versions +- minor improvement: diagnostics TLS firewall/proxy detection +- minor improvement: full certificate chain is now behind `--debug` flag + ## v16.1.1 (date: 5.9.2023) WORK IN PROGRESS - bug fix: added missing proxies to micromamba phase diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 06d7e399..61538fe0 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -151,10 +151,18 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { } result.Details["dns-lookup-time"] = dnsStopwatch.Text() tlsStopwatch := common.Stopwatch("TLS verification time for %d hostnames was about", len(hostnames)) + tlsRoots := make(map[string]bool) for _, host := range hostnames { - result.Checks = append(result.Checks, tlsCheckHost(host)...) + result.Checks = append(result.Checks, tlsCheckHost(host, tlsRoots)...) } result.Details["tls-lookup-time"] = tlsStopwatch.Text() + if len(hostnames) > 1 && len(tlsRoots) == 1 { + for name, _ := range tlsRoots { + result.Details["tls-proxy-firewall"] = name + } + } else { + result.Details["tls-proxy-firewall"] = "undetectable" + } result.Checks = append(result.Checks, canaryDownloadCheck()) result.Checks = append(result.Checks, pypiHeadCheck()) result.Checks = append(result.Checks, condaHeadCheck()) diff --git a/operations/netdiagnostics.go b/operations/netdiagnostics.go index f984c772..86856744 100644 --- a/operations/netdiagnostics.go +++ b/operations/netdiagnostics.go @@ -75,11 +75,19 @@ func networkDiagnostics(config *Configuration, target *common.DiagnosticStatus) target.Checks = append(target.Checks, dnsLookupCheck(host)) } target.Details["dns-lookup-time"] = dnsStopwatch.Text() + tlsRoots := make(map[string]bool) tlsStopwatch := common.Stopwatch("TLS verification time for %d hostnames was about", len(hostnames)) for _, host := range hostnames { - target.Checks = append(target.Checks, tlsCheckHost(host)...) + target.Checks = append(target.Checks, tlsCheckHost(host, tlsRoots)...) } target.Details["tls-lookup-time"] = tlsStopwatch.Text() + if len(hostnames) > 1 && len(tlsRoots) == 1 { + for name, _ := range tlsRoots { + target.Details["tls-proxy-firewall"] = name + } + } else { + target.Details["tls-proxy-firewall"] = "undetectable" + } headStopwatch := common.Stopwatch("HEAD request time for %d requests was about", len(config.Network.Head)) for _, entry := range config.Network.Head { target.Checks = append(target.Checks, webDiagnostics("HEAD", common.CategoryNetworkHEAD, headRequest, entry, supportUrl)...) diff --git a/operations/tlscheck.go b/operations/tlscheck.go index 84afcac4..eb9747b9 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -26,6 +26,7 @@ func init() { func get(url string) (*tls.ConnectionState, error) { transport := settings.Global.ConfiguredHttpTransport() transport.TLSClientConfig.InsecureSkipVerify = true + transport.TLSClientConfig.MinVersion = tls.VersionSSL30 client := http.Client{Transport: transport} response, err := client.Head(url) if err != nil { @@ -46,7 +47,7 @@ func certificateChain(certificates []*x509.Certificate) string { return strings.Join(parts, "; ") } -func tlsCheckHost(host string) []*common.DiagnosticCheck { +func tlsCheckHost(host string, roots map[string]bool) []*common.DiagnosticCheck { transport := settings.Global.ConfiguredHttpTransport() result := []*common.DiagnosticCheck{} supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") @@ -107,6 +108,7 @@ func tlsCheckHost(host string) []*common.DiagnosticCheck { last = certificate } _, err = certificates[0].Verify(toVerify) + roots[last.Issuer.String()] = err == nil if err != nil { result = append(result, &common.DiagnosticCheck{ Type: "network", @@ -115,13 +117,15 @@ func tlsCheckHost(host string) []*common.DiagnosticCheck { Message: fmt.Sprintf("TLS verification of %q failed, reason: %v [last issuer: %q]", server, err, last.Issuer), Link: supportNetworkUrl, }) - result = append(result, &common.DiagnosticCheck{ - Type: "network", - Category: common.CategoryNetworkTLSChain, - Status: statusWarning, - Message: fmt.Sprintf("%q certificate chain is {%s}.", host, certificateChain(certificates)), - Link: supportNetworkUrl, - }) + if common.DebugFlag() { + result = append(result, &common.DiagnosticCheck{ + Type: "network", + Category: common.CategoryNetworkTLSChain, + Status: statusWarning, + Message: fmt.Sprintf("%q certificate chain is {%s}.", host, certificateChain(certificates)), + Link: supportNetworkUrl, + }) + } } else { result = append(result, &common.DiagnosticCheck{ Type: "network", From 0a5aff22384986afebb96c1de26a11fa9252a297 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 7 Sep 2023 08:39:28 +0300 Subject: [PATCH 426/516] Comment on CodeQL security warning code (v16.1.3) - comment explaining why certain unsecure code forms is required when TLS diagnostics are done (to explain Github CodeQL security warnings) --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/tlscheck.go | 10 ++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 71a04096..9d413425 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.1.2` + Version = `v16.1.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 33fc4072..d31d2331 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.1.3 (date: 7.9.2023) + +- comment explaining why certain unsecure code forms is required when + TLS diagnostics are done (to explain Github CodeQL security warnings) + ## v16.1.2 (date: 6.9.2023) WORK IN PROGRESS - bug fix: allowing detection of lower levels of TLS versions diff --git a/operations/tlscheck.go b/operations/tlscheck.go index eb9747b9..3afa6adc 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -23,10 +23,16 @@ func init() { tlsVersions[tls.VersionTLS13] = "TLS 1.3" } -func get(url string) (*tls.ConnectionState, error) { +func tlsCheckHeadOnly(url string) (*tls.ConnectionState, error) { transport := settings.Global.ConfiguredHttpTransport() transport.TLSClientConfig.InsecureSkipVerify = true transport.TLSClientConfig.MinVersion = tls.VersionSSL30 + // above two setting are needed for TLS checks + // they weaken security, and that is why this code is only used + // to get TLS connection state and nothing else + // this is intentional, so that network diagnosis can detect + // unsecure certificates, and connections to weaker TLS version + // [ref: Github CodeQL security warning] client := http.Client{Transport: transport} response, err := client.Head(url) if err != nil { @@ -52,7 +58,7 @@ func tlsCheckHost(host string, roots map[string]bool) []*common.DiagnosticCheck result := []*common.DiagnosticCheck{} supportNetworkUrl := settings.Global.DocsLink("troubleshooting/firewall-and-proxies") url := fmt.Sprintf("https://%s/", host) - state, err := get(url) + state, err := tlsCheckHeadOnly(url) if err != nil { result = append(result, &common.DiagnosticCheck{ Type: "network", From ae271917e9714600aac14c2d9fda0027ceac126f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 11 Sep 2023 15:46:30 +0300 Subject: [PATCH 427/516] Feature: relocation stats on catalogs list (v16.2.0) - added relocations statistics on catalog listing (Relocate column) --- cmd/holotreeCatalogs.go | 9 +++++---- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/functions.go | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cmd/holotreeCatalogs.go b/cmd/holotreeCatalogs.go index 29de786c..b41cf1f9 100644 --- a/cmd/holotreeCatalogs.go +++ b/cmd/holotreeCatalogs.go @@ -96,6 +96,7 @@ func jsonCatalogDetails(roots []*htfs.Root, topN int) { data["directories"] = stats.Directories data["files"] = stats.Files data["bytes"] = stats.Bytes + data["relocations"] = stats.Relocations holder[catalog.Blueprint] = data age, _ := pathlib.DaysSinceModified(catalog.Source()) data["age_in_days"] = age @@ -132,8 +133,8 @@ func dumpTopN(stats map[string]int64, total float64, tabbed *tabwriter.Writer) { func listCatalogDetails(roots []*htfs.Root, topN int) { used := catalogUsedStats() tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) - tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tidentity.yaml (gzipped blob inside hololib)\tHolotree path\tAge (days)\tIdle (days)\n")) - tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t-------------------------------------------\t-------------\t----------\t-----------\n")) + tabbed.Write([]byte("Blueprint\tPlatform\tDirs \tFiles \tSize \tRelocate\tidentity.yaml (gzipped blob inside hololib)\tHolotree path\tAge (days)\tIdle (days)\n")) + tabbed.Write([]byte("---------\t--------\t------\t-------\t-------\t--------\t-------------------------------------------\t-------------\t----------\t-----------\n")) for _, catalog := range roots { lastUse, ok := used[catalog.Blueprint] if !ok { @@ -143,11 +144,11 @@ func listCatalogDetails(roots []*htfs.Root, topN int) { stats, err := catalog.Stats() pretty.Guard(err == nil, 1, "Could not get stats for %s, reason: %s", catalog.Blueprint, err) days, _ := pathlib.DaysSinceModified(catalog.Source()) - data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t%s\t%s\t%10d\t%11d\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Identity, catalog.HolotreeBase(), days, lastUse) + data := fmt.Sprintf("%s\t%s\t% 6d\t% 7d\t% 6dM\t% 8d\t%s\t%s\t%10d\t%11d\n", catalog.Blueprint, catalog.Platform, stats.Directories, stats.Files, megas(stats.Bytes), stats.Relocations, stats.Identity, catalog.HolotreeBase(), days, lastUse) tabbed.Write([]byte(data)) if showIdentityYaml { for _, line := range identityContentLines(catalog) { - tabbed.Write([]byte(fmt.Sprintf("\t\t\t\t\t%s\n", line))) + tabbed.Write([]byte(fmt.Sprintf("\t\t\t\t\t\t%s\n", line))) } } if topN > 0 { diff --git a/common/version.go b/common/version.go index 9d413425..d34a652d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.1.3` + Version = `v16.2.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index d31d2331..991786f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v16.2.0 (date: 11.9.2023) + +- added relocations statistics on catalog listing (Relocate column) + ## v16.1.3 (date: 7.9.2023) - comment explaining why certain unsecure code forms is required when diff --git a/htfs/functions.go b/htfs/functions.go index 10b325e3..7a15e212 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -309,6 +309,7 @@ type TreeStats struct { Files uint64 Bytes uint64 Identity string + Relocations uint64 } func guessLocation(digest string) string { @@ -328,6 +329,9 @@ func CalculateTreeStats() (Dirtask, *TreeStats) { if file.Name == "identity.yaml" { result.Identity = guessLocation(file.Digest) } + if len(file.Rewrite) > 0 { + result.Relocations += 1 + } } } }, result From 61abd7596b25da9c659adb4ada91fd333ae56dba Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 12 Sep 2023 11:42:56 +0300 Subject: [PATCH 428/516] Bugfix: infinite process tree printing (v16.2.1) - bugfix: detecting and truncating process tree with too deep child structure --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/processtree.go | 18 ++++++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index d34a652d..2396aafa 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.2.0` + Version = `v16.2.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 991786f8..42420c58 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v16.2.1 (date: 12.9.2023) + +- bugfix: detecting and truncating process tree with too deep child structure + ## v16.2.0 (date: 11.9.2023) - added relocations statistics on catalog listing (Relocate column) diff --git a/operations/processtree.go b/operations/processtree.go index affdb163..e6c74cef 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -67,15 +67,15 @@ func (it ProcessMap) Roots() []int { func (it *ProcessNode) warnings(additional ProcessMap) { if len(it.Children) > 0 { - pretty.Warning("%q process %d still has running subprocesses:", it.Executable, it.Pid) - it.warningTree("> ", false) + pretty.Warning("%q process %d (parent %d) still has running subprocesses:", it.Executable, it.Pid, it.Parent) + it.warningTree("> ", false, 20) } else { - pretty.Warning("%q process %d still has running migrated processes:", it.Executable, it.Pid) + pretty.Warning("%q process %d (parent %d) still has running migrated processes:", it.Executable, it.Pid, it.Parent) } if len(additional) > 0 { pretty.Warning("+ migrated process still running:") for _, key := range additional.Roots() { - additional[key].warningTree("| ", true) + additional[key].warningTree("| ", true, 20) } } pretty.Note("Depending on OS, above processes may prevent robot to close properly.") @@ -89,19 +89,25 @@ func (it *ProcessNode) warnings(additional ProcessMap) { pretty.Highlight("Example cleanup command: %s", common.GenerateKillCommand(additional.Keys())) } -func (it *ProcessNode) warningTree(prefix string, newparent bool) { +func (it *ProcessNode) warningTree(prefix string, newparent bool, limit int) { kind := "leaf" if len(it.Children) > 0 { kind = "container" } if newparent { kind = fmt.Sprintf("%s -> new parent PID: #%d", kind, it.Parent) + } else { + kind = fmt.Sprintf("%s under #%d", kind, it.Parent) } pretty.Warning("%s#%d %q <%s>", prefix, it.Pid, it.Executable, kind) common.RunJournal("orphan process", fmt.Sprintf("parent=%d pid=%d name=%s", it.Parent, it.Pid, it.Executable), "process pollution") + if limit < 0 { + pretty.Warning("%s Maximum recursion depth detected. Truncating output here.", prefix) + return + } indent := prefix + "| " for _, key := range it.Children.Keys() { - it.Children[key].warningTree(indent, false) + it.Children[key].warningTree(indent, false, limit-1) } } From 719fb65f14ffaf74c5af7e9350aa2238afe57ad8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 15 Sep 2023 09:14:35 +0300 Subject: [PATCH 429/516] Bugfix: small delay before process tree printing (v16.2.2) - bugfix: process tree 1 second delay to prevent "too fast" process snapshots on Windows - refactoring some unused code out of codebase --- common/version.go | 2 +- conda/workflows.go | 46 +++++++++++++++++++-------------------- docs/changelog.md | 6 +++++ htfs/commands.go | 2 ++ operations/processtree.go | 1 + 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/common/version.go b/common/version.go index 2396aafa..214160ac 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.2.1` + Version = `v16.2.2` ) diff --git a/conda/workflows.go b/conda/workflows.go index fdfa8789..9f811874 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -390,38 +390,36 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return true, false } -func mergedEnvironment(filenames ...string) (right *Environment, err error) { - for _, filename := range filenames { - left := right - right, err = ReadCondaYaml(filename) - if err != nil { - return nil, err - } - if left == nil { - continue - } - right, err = left.Merge(right) - if err != nil { - return nil, err - } +func LogUnifiedEnvironment(content []byte) { + environment, err := CondaYamlFrom(content) + if err != nil { + return + } + yaml, err := environment.AsYaml() + if err != nil { + return } - return right, nil + common.Log("FINAL unified conda environment descriptor:\n---\n%v---", yaml) } -func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { - right, err := mergedEnvironment(filenames...) +func finalUnifiedEnvironment(filename string, verbose bool) (string, *Environment, error) { + right, err := ReadCondaYaml(filename) if err != nil { - return "", "", nil, err + return "", nil, err } yaml, err := right.AsYaml() + if err != nil { + return "", nil, err + } + return yaml, right, nil +} + +func temporaryConfig(condaYaml, requirementsText, filename string) (string, string, *Environment, error) { + yaml, right, err := finalUnifiedEnvironment(filename, true) if err != nil { return "", "", nil, err } hash := common.ShortDigest(yaml) - if !save { - return hash, yaml, right, nil - } - common.Log("FINAL union conda environment descriptor:\n---\n%v---", yaml) err = right.SaveAsRequirements(requirementsText) if err != nil { return "", "", nil, err @@ -431,7 +429,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. return hash, yaml, right, err } -func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configurations ...string) error { +func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configuration string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() @@ -449,7 +447,7 @@ func LegacyEnvironment(recorder Recorder, force bool, skip SkipLayer, configurat condaYaml := filepath.Join(pathlib.TempDir(), fmt.Sprintf("conda_%x.yaml", common.When)) requirementsText := filepath.Join(pathlib.TempDir(), fmt.Sprintf("require_%x.txt", common.When)) common.Debug("Using temporary conda.yaml file: %v and requirement.txt file: %v", condaYaml, requirementsText) - key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, true, configurations...) + key, yaml, finalEnv, err := temporaryConfig(condaYaml, requirementsText, configuration) if err != nil { return err } diff --git a/docs/changelog.md b/docs/changelog.md index 42420c58..0c225b7b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v16.2.2 (date: 13.9.2023) + +- bugfix: process tree 1 second delay to prevent "too fast" process snapshots + on Windows +- refactoring some unused code out of codebase + ## v16.2.1 (date: 12.9.2023) - bugfix: detecting and truncating process tree with too deep child structure diff --git a/htfs/commands.go b/htfs/commands.go index 19411d10..5df62650 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -125,6 +125,8 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec exists := tree.HasBlueprint(blueprint) common.Debug("Has blueprint environment: %v", exists) + conda.LogUnifiedEnvironment(blueprint) + if force || !exists { common.FreshlyBuildEnvironment = true remoteOrigin := common.RccRemoteOrigin() diff --git a/operations/processtree.go b/operations/processtree.go index e6c74cef..12504784 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -117,6 +117,7 @@ func SubprocessWarning(seen ChildMap, use bool) error { common.Debug("No tracked subprocesses, which is a good thing.") return nil } + time.Sleep(1 * time.Second) // small nap to let things settle before asking all processes processes, err := ProcessMapNow() if err != nil { return err From 16e1ae5b5d082af13d72d946c5df81b14fe3c310 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 19 Sep 2023 11:27:48 +0300 Subject: [PATCH 430/516] Feature: rcc point of view extended (v16.3.0) - extended using "rcc point of view" messaging to environment building, post-install and pre-run scripts - holotree variables also now has "rcc point of view" visible - changed robot tests to match "rcc point of view" changes - highlighted Progress steps with cyan/green/red color (where available) --- cmd/holotreeVariables.go | 5 +++ common/logger.go | 10 ------ common/variables.go | 2 -- common/version.go | 2 +- conda/workflows.go | 34 +++++++++++++------- docs/changelog.md | 8 +++++ htfs/commands.go | 30 ++++++++++-------- htfs/library.go | 2 +- operations/running.go | 25 +++++---------- pretty/functions.go | 54 ++++++++++++++++++++++++++++++++ robot_tests/export_holozip.robot | 2 +- robot_tests/fullrun.robot | 4 +-- robot_tests/templates.robot | 8 ++--- 13 files changed, 122 insertions(+), 64 deletions(-) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 9e8f6663..5f615f21 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -16,6 +16,10 @@ import ( "github.com/spf13/cobra" ) +const ( + newEnvironment = `environment creation` +) + var ( holotreeBlueprint []byte holotreeForce bool @@ -80,6 +84,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp holozip = config.Holozip() } path, _, err := htfs.NewEnvironment(condafile, holozip, true, force, operations.PullCatalog) + pretty.RccPointOfView(newEnvironment, err) pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/common/logger.go b/common/logger.go index 93ee2518..31ded87b 100644 --- a/common/logger.go +++ b/common/logger.go @@ -98,13 +98,3 @@ func WaitLogs() { runtime.Gosched() logbarrier.Wait() } - -func Progress(step int, form string, details ...interface{}) { - previous := ProgressMark - ProgressMark = time.Now() - delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() - message := fmt.Sprintf(form, details...) - Log("#### Progress: %02d/15 %s %8.3fs %s", step, Version, delta, message) - Timeline("%d/15 %s", step, message) - RunJournal("environment", "build", "Progress: %02d/15 %s %8.3fs %s", step, Version, delta, message) -} diff --git a/common/variables.go b/common/variables.go index 2f826cd3..60289866 100644 --- a/common/variables.go +++ b/common/variables.go @@ -53,7 +53,6 @@ var ( SemanticTag string ForcedRobocorpHome string When int64 - ProgressMark time.Time Clock *stopwatch randomIdentifier string verbosity Verbosity @@ -62,7 +61,6 @@ var ( func init() { Clock = &stopwatch{"Clock", time.Now()} When = Clock.When() - ProgressMark = time.Now() randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) diff --git a/common/version.go b/common/version.go index 214160ac..8617c1ab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.2.2` + Version = `v16.3.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 9f811874..091249f8 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -27,6 +27,12 @@ const ( SkipError SkipLayer = iota ) +const ( + micromambaInstall = `micromamba install` + pipInstall = `pip install` + postInstallScripts = `post-install script execution` +) + type ( SkipLayer uint8 Recorder interface { @@ -173,7 +179,7 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. if force { ttl = "0" } - common.Progress(7, "Running micromamba phase. (micromamba v%s) [layer: %s]", MicromambaVersion(), fingerprint) + pretty.Progress(7, "Running micromamba phase. (micromamba v%s) [layer: %s]", MicromambaVersion(), fingerprint) mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-env", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -188,6 +194,7 @@ func micromambaLayer(fingerprint, condaYaml, targetFolder string, stopwatch fmt. cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.micromamba", fmt.Sprintf("%d_%x", code, code)) common.Timeline("micromamba fail.") common.Fatal(fmt.Sprintf("Micromamba [%d/%x]", code, code), err) + pretty.RccPointOfView(micromambaInstall, err) return false, false } journal.CurrentBuildEvent().MicromambaComplete() @@ -212,7 +219,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. pipCache, wheelCache := common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - common.Progress(8, "Skipping pip install phase -- no pip dependencies.") + pretty.Progress(8, "Skipping pip install phase -- no pip dependencies.") } else { if !pyok { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", 9999, 9999)) @@ -220,7 +227,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. common.Fatal("pip fail. no python found.", errors.New("No python found, but required!")) return false, false, pipUsed, "" } - common.Progress(8, "Running pip install phase. (pip v%s) [layer: %s]", PipVersion(python), fingerprint) + pretty.Progress(8, "Running pip install phase. (pip v%s) [layer: %s]", PipVersion(python), fingerprint) common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander(python, "-m", "pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -232,6 +239,7 @@ func pipLayer(fingerprint, requirementsText, targetFolder string, stopwatch fmt. cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") common.Fatal(fmt.Sprintf("Pip [%d/%x]", code, code), err) + pretty.RccPointOfView(pipInstall, err) return false, false, pipUsed, "" } journal.CurrentBuildEvent().PipComplete() @@ -248,13 +256,14 @@ func postInstallLayer(fingerprint string, postInstall []string, targetFolder str fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - common.Progress(9, "Post install scripts phase started. [layer: %s]", fingerprint) + pretty.Progress(9, "Post install scripts phase started. [layer: %s]", fingerprint) common.Debug("=== post install phase ===") for _, script := range postInstall { scriptCommand, err := shell.Split(script) if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) + pretty.RccPointOfView(postInstallScripts, err) return false, false } common.Debug("Running post install script '%s' ...", script) @@ -262,12 +271,13 @@ func postInstallLayer(fingerprint string, postInstall []string, targetFolder str if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) + pretty.RccPointOfView(postInstallScripts, err) return false, false } } journal.CurrentBuildEvent().PostInstallComplete() } else { - common.Progress(9, "Post install scripts phase skipped -- no scripts.") + pretty.Progress(9, "Post install scripts phase skipped -- no scripts.") } return true, false } @@ -298,7 +308,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t recorder.Record([]byte(layers[0])) } } else { - common.Progress(7, "Skipping micromamba phase, layer exists.") + pretty.Progress(7, "Skipping micromamba phase, layer exists.") } if skip < SkipPipLayer { success, fatal, pipUsed, python = pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter) @@ -312,7 +322,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t recorder.Record([]byte(layers[1])) } } else { - common.Progress(8, "Skipping pip phase, layer exists.") + pretty.Progress(8, "Skipping pip phase, layer exists.") } if skip < SkipPostinstallLayer { success, fatal = postInstallLayer(fingerprints[2], finalEnv.PostInstall, targetFolder, stopwatch, planWriter) @@ -320,7 +330,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t return success, fatal, pipUsed, python } } else { - common.Progress(9, "Skipping post install scripts phase, layer exists.") + pretty.Progress(9, "Skipping post install scripts phase, layer exists.") } return true, false, pipUsed, python } @@ -349,7 +359,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return success, fatal } - common.Progress(10, "Activate environment started phase.") + pretty.Progress(10, "Activate environment started phase.") common.Debug("=== activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) err := Activate(planWriter, targetFolder) @@ -365,7 +375,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- pip check plan @%ss ---\n\n", stopwatch) if common.StrictFlag && pipUsed { - common.Progress(11, "Running pip check phase.") + pretty.Progress(11, "Running pip check phase.") pipCommand := common.NewCommander(python, "-m", "pip", "check", "--no-color") pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== pip check phase ===") @@ -378,10 +388,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } common.Timeline("pip check done.") } else { - common.Progress(11, "Pip check skipped.") + pretty.Progress(11, "Pip check skipped.") } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) - common.Progress(12, "Update installation plan.") + pretty.Progress(12, "Update installation plan.") common.Error("saving rcc_plan.log", theplan.Save()) common.Debug("=== finalize phase ===") diff --git a/docs/changelog.md b/docs/changelog.md index 0c225b7b..232d5943 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v16.3.0 (date: 19.9.2023) + +- extended using "rcc point of view" messaging to environment building, + post-install and pre-run scripts +- holotree variables also now has "rcc point of view" visible +- changed robot tests to match "rcc point of view" changes +- highlighted Progress steps with cyan/green/red color (where available) + ## v16.2.2 (date: 13.9.2023) - bugfix: process tree 1 second delay to prevent "too fast" process snapshots diff --git a/htfs/commands.go b/htfs/commands.go index 5df62650..77fbb6d0 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -35,7 +35,11 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal path := "" defer func() { - common.Progress(15, "Fresh holotree done [with %d workers].", anywork.Scale()) + if err != nil { + pretty.Regression(15, "Holotree restoration failure, see above [with %d workers].", anywork.Scale()) + } else { + pretty.Progress(15, "Fresh holotree done [with %d workers].", anywork.Scale()) + } if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) } @@ -49,9 +53,9 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } }() if common.SharedHolotree { - common.Progress(1, "Fresh [shared mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) + pretty.Progress(1, "Fresh [shared mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) } else { - common.Progress(1, "Fresh [private mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) + pretty.Progress(1, "Fresh [private mode] holotree environment %v. (parent/pid: %d/%d)", xviper.TrackingIdentity(), os.Getppid(), os.Getpid()) } lockfile := common.HolotreeLock() @@ -64,7 +68,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false - common.Progress(2, "Holotree blueprint is %q [%s with %d workers].", common.EnvironmentHash, common.Platform(), anywork.Scale()) + pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers].", common.EnvironmentHash, common.Platform(), anywork.Scale()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() @@ -93,12 +97,12 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } if restore { - common.Progress(14, "Restore space from library [with %d workers].", anywork.Scale()) + pretty.Progress(14, "Restore space from library [with %d workers].", anywork.Scale()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() } else { - common.Progress(14, "Restoring space skipped.") + pretty.Progress(14, "Restoring space skipped.") } return path, scorecard, nil @@ -131,7 +135,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec common.FreshlyBuildEnvironment = true remoteOrigin := common.RccRemoteOrigin() if len(remoteOrigin) > 0 { - common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN.") + pretty.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN.") hash := common.BlueprintHash(blueprint) catalog := CatalogName(hash) err = puller(remoteOrigin, catalog, false) @@ -142,9 +146,9 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec } exists = tree.HasBlueprint(blueprint) } else { - common.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN skipped. RCC_REMOTE_ORIGIN was not defined.") + pretty.Progress(3, "Fill hololib from RCC_REMOTE_ORIGIN skipped. RCC_REMOTE_ORIGIN was not defined.") } - common.Progress(4, "Cleanup holotree stage for fresh install.") + pretty.Progress(4, "Cleanup holotree stage for fresh install.") fail.On(settings.Global.NoBuild(), "Building new holotree environment is blocked by settings, and could not be found from hololib cache!") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) @@ -153,17 +157,17 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) - common.Progress(5, "Build environment into holotree stage %q.", tree.Stage()) + pretty.Progress(5, "Build environment into holotree stage %q.", tree.Stage()) identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = os.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) skip := conda.SkipNoLayers if !force && common.LayeredHolotree { - common.Progress(6, "Restore partial environment into holotree stage %q.", tree.Stage()) + pretty.Progress(6, "Restore partial environment into holotree stage %q.", tree.Stage()) skip = RestoreLayersTo(tree, identityfile, tree.Stage()) } else { - common.Progress(6, "Restore partial environment skipped. Layers disabled or force used.") + pretty.Progress(6, "Restore partial environment skipped. Layers disabled or force used.") } err = os.WriteFile(identityfile, blueprint, 0o644) @@ -174,7 +178,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec scorecard.Midpoint() - common.Progress(13, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) + pretty.Progress(13, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) journal.CurrentBuildEvent().RecordComplete() diff --git a/htfs/library.go b/htfs/library.go index b7d7a49d..2e78fba7 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -323,7 +323,7 @@ func ControllerSpaceName(client, tag []byte) string { func touchUsedHash(hash string) { filename := fmt.Sprintf("%s.%s", hash, common.UserHomeIdentity()) fullpath := filepath.Join(common.HololibUsageLocation(), filename) - pathlib.ForceTouchWhen(fullpath, common.ProgressMark) + pathlib.ForceTouchWhen(fullpath, pretty.ProgressMark) } func (it *hololib) TargetDir(blueprint, controller, space []byte) (result string, err error) { diff --git a/operations/running.go b/operations/running.go index d85b79c7..391c0dc2 100644 --- a/operations/running.go +++ b/operations/running.go @@ -19,7 +19,9 @@ import ( ) const ( - rccpov = `From rcc point of view, actual main robot run was` + actualRun = `actual main robot run` + preRun = `pre-run script execution` + newEnvironment = `environment creation` ) var ( @@ -182,6 +184,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. label, _, err := htfs.NewEnvironment(config.CondaConfigFile(), config.Holozip(), true, force, PullCatalog) if err != nil { + pretty.RccPointOfView(newEnvironment, err) pretty.Exit(4, "Error: %v", err) } return false, config, todo, label @@ -332,12 +335,14 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } scriptCommand, err := shell.Split(script) if err != nil { + pretty.RccPointOfView(preRun, err) pretty.Exit(11, "%sScript '%s' parsing failure: %v%s", pretty.Red, script, err, pretty.Reset) } scriptCommand[0] = findExecutableOrDie(searchPath, scriptCommand[0]) common.Debug("Running pre run script '%s' ...", script) _, err = shell.New(environment, directory, scriptCommand...).Execute(interactive) if err != nil { + pretty.RccPointOfView(preRun, err) pretty.Exit(12, "%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) } } @@ -355,7 +360,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) } }) - showRccPointOfView(err) + pretty.RccPointOfView(actualRun, err) seen, ok := <-pipe suberr := SubprocessWarning(seen, ok) if suberr != nil { @@ -370,19 +375,3 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro } pretty.Ok() } - -func showRccPointOfView(err error) { - printer := pretty.Lowlight - message := fmt.Sprintf("@@@ %s SUCCESS. @@@", rccpov) - journal := fmt.Sprintf("%s SUCCESS.", rccpov) - if err != nil { - printer = pretty.Highlight - message = fmt.Sprintf("@@@ %s FAILURE, reason: %q. See details above. @@@", rccpov, err) - journal = fmt.Sprintf("%s FAILURE, reason: %s", rccpov, err) - } - banner := strings.Repeat("@", len(message)) - printer(banner) - printer(message) - printer(banner) - common.RunJournal("robot exit", journal, "rcc point of view") -} diff --git a/pretty/functions.go b/pretty/functions.go index b4dc5a14..2d94415c 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -2,10 +2,25 @@ package pretty import ( "fmt" + "strings" + "time" "github.com/robocorp/rcc/common" ) +const ( + rccpov = `From rcc point of view, %q was` + maxSteps = 15 +) + +var ( + ProgressMark time.Time +) + +func init() { + ProgressMark = time.Now() +} + func Ok() error { common.Log("%sOK.%s", Green, Reset) return nil @@ -52,3 +67,42 @@ func Guard(truth bool, code int, format string, rest ...interface{}) { Exit(code, format, rest...) } } + +func RccPointOfView(context string, err error) { + explain := fmt.Sprintf(rccpov, context) + printer := Lowlight + message := fmt.Sprintf("@@@ %s SUCCESS. @@@", explain) + journal := fmt.Sprintf("%s SUCCESS.", explain) + if err != nil { + printer = Highlight + message = fmt.Sprintf("@@@ %s FAILURE, reason: %q. See details above. @@@", explain, err) + journal = fmt.Sprintf("%s FAILURE, reason: %s", explain, err) + } + banner := strings.Repeat("@", len(message)) + printer(banner) + printer(message) + printer(banner) + common.RunJournal("robot exit", journal, "rcc point of view") +} + +func Regression(step int, form string, details ...interface{}) { + progress(Red, step, form, details...) +} + +func Progress(step int, form string, details ...interface{}) { + color := Cyan + if step == maxSteps { + color = Green + } + progress(color, step, form, details...) +} + +func progress(color string, step int, form string, details ...interface{}) { + previous := ProgressMark + ProgressMark = time.Now() + delta := ProgressMark.Sub(previous).Round(1 * time.Millisecond).Seconds() + message := fmt.Sprintf(form, details...) + common.Log("%s#### Progress: %02d/%d %s %8.3fs %s%s", color, step, maxSteps, common.Version, delta, message, Reset) + common.Timeline("%d/%d %s", step, maxSteps, message) + common.RunJournal("environment", "build", "Progress: %02d/%d %s %8.3fs %s", step, maxSteps, common.Version, delta, message) +} diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index d2ad0bcb..194a7ad6 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -86,7 +86,7 @@ Goal: Can run as guest Prepare Robocorp Home tmp/guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Space created under author for guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index a3afb032..fe820f3c 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -96,7 +96,7 @@ Goal: Run task in place in debug mode and with timeline. Must Have Version Must Have Origin Must Have Status - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Must Exist tmp/fluffy/output/environment_*_freeze.yaml Must Exist %{ROBOCORP_HOME}/wheels/ @@ -115,7 +115,7 @@ Goal: Run task in clean temporary directory. Wont Have Progress: 09/15 Must Have Progress: 14/15 Must Have Progress: 15/15 - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index 07327801..092649c0 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -17,7 +17,7 @@ Goal: Standard robot has correct hash. Goal: Running standard robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/standardi/robot.yaml Use STDERR - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Initialize new python robot. @@ -32,7 +32,7 @@ Goal: Python robot has correct hash. Goal: Running python robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/pythoni/robot.yaml Use STDERR - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Initialize new extended robot. @@ -47,13 +47,13 @@ Goal: Extended robot has correct hash. Goal: Running extended robot is succesful. (Run All Tasks) Step build/rcc task run --space templates --task "Run All Tasks" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Running extended robot is succesful. (Run Example Task) Step build/rcc task run --space templates --task "Run Example Task" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR - Must Have From rcc point of view, actual main robot run was SUCCESS. + Must Have From rcc point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Correct holotree spaces were created. From 28278ee0ea61adfd883a7d584bdc84636990423c Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 20 Sep 2023 10:23:23 +0300 Subject: [PATCH 431/516] Bugfix: big template extraction (v16.3.1) - bug fix: extracting big template failed - now some Progress steps have CPUs also visible, in addition to worker count --- cmd/speed.go | 3 ++- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 11 ++++++----- operations/zipper.go | 7 ++++--- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cmd/speed.go b/cmd/speed.go index 99cf7a7b..a1d9d5a9 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -5,6 +5,7 @@ import ( "math/rand" "os" "path/filepath" + "runtime" "time" "github.com/robocorp/rcc/anywork" @@ -57,7 +58,7 @@ var speedtestCmd = &cobra.Command{ if common.DebugFlag() { defer common.Stopwatch("Speed test run lasted").Report() } - common.Log("Running network and filesystem performance tests with %d workers.", anywork.Scale()) + common.Log("Running network and filesystem performance tests with %d workers on %d CPUs.", anywork.Scale(), runtime.NumCPU()) common.Log("This may take several minutes, please be patient.") signal := make(chan bool) timing := make(chan int) diff --git a/common/version.go b/common/version.go index 8617c1ab..2bb1e014 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.3.0` + Version = `v16.3.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 232d5943..4fd099f5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.3.1 (date: 20.9.2023) + +- bug fix: extracting big template failed +- now some Progress steps have CPUs also visible, in addition to worker count + ## v16.3.0 (date: 19.9.2023) - extended using "rcc point of view" messaging to environment building, diff --git a/htfs/commands.go b/htfs/commands.go index 77fbb6d0..15d696bb 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -3,6 +3,7 @@ package htfs import ( "os" "path/filepath" + "runtime" "strings" "github.com/robocorp/rcc/anywork" @@ -36,9 +37,9 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal path := "" defer func() { if err != nil { - pretty.Regression(15, "Holotree restoration failure, see above [with %d workers].", anywork.Scale()) + pretty.Regression(15, "Holotree restoration failure, see above [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) } else { - pretty.Progress(15, "Fresh holotree done [with %d workers].", anywork.Scale()) + pretty.Progress(15, "Fresh holotree done [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) } if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) @@ -68,7 +69,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false - pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers].", common.EnvironmentHash, common.Platform(), anywork.Scale()) + pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers on %d CPUs].", common.EnvironmentHash, common.Platform(), anywork.Scale(), runtime.NumCPU()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() @@ -97,7 +98,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal } if restore { - pretty.Progress(14, "Restore space from library [with %d workers].", anywork.Scale()) + pretty.Progress(14, "Restore space from library [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) path, err = library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) journal.CurrentBuildEvent().RestoreComplete() @@ -178,7 +179,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec scorecard.Midpoint() - pretty.Progress(13, "Record holotree stage to hololib [with %d workers].", anywork.Scale()) + pretty.Progress(13, "Record holotree stage to hololib [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) journal.CurrentBuildEvent().RecordComplete() diff --git a/operations/zipper.go b/operations/zipper.go index 35521c9e..4c9d6c81 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -2,6 +2,7 @@ package operations import ( "archive/zip" + "bytes" "fmt" "io" "os" @@ -187,15 +188,15 @@ func (it *unzipper) Asset(name string) ([]byte, error) { if err != nil { return nil, err } - payload := make([]byte, stat.Size()) - total, err := stream.Read(payload) + payload := bytes.NewBuffer(nil) + total, err := io.Copy(payload, stream) if err != nil && err != io.EOF { return nil, err } if int64(total) != stat.Size() { pretty.Warning("Asset %q read partially!", name) } - return payload, nil + return payload.Bytes(), nil } func (it *unzipper) ExtraDirectoryPrefixLength() (int, string) { From 9db2ff3271b44aca2eb5de0cf83891ace9c78f53 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 21 Sep 2023 08:39:06 +0300 Subject: [PATCH 432/516] Internal: TLS probe (v16.4.0) - feature: internal TLS probe implementation --- cmd/internalProbe.go | 37 +++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 ++ operations/tlscheck.go | 105 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 cmd/internalProbe.go diff --git a/cmd/internalProbe.go b/cmd/internalProbe.go new file mode 100644 index 00000000..e9ee9092 --- /dev/null +++ b/cmd/internalProbe.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "strings" + + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +func fixHosts(hosts []string) []string { + servers := make([]string, len(hosts)) + for at, host := range hosts { + if strings.Contains(host, ":") { + servers[at] = host + } else { + servers[at] = host + ":443" + } + } + return servers +} + +var internalProbeCmd = &cobra.Command{ + Use: "probe +", + Short: "Probe host:port combinations for supported TLS versions.", + Long: "Probe host:port combinations for supported TLS versions.", + Run: func(cmd *cobra.Command, args []string) { + servers := fixHosts(args) + err := operations.TLSProbe(servers) + pretty.Guard(err == nil, 1, "Probe failure: %v", err) + pretty.Ok() + }, +} + +func init() { + internalCmd.AddCommand(internalProbeCmd) +} diff --git a/common/version.go b/common/version.go index 2bb1e014..5ecfe607 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.3.1` + Version = `v16.4.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 4fd099f5..19cffd81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v16.4.0 (date: 21.9.2023) INTERNAL + +- feature: internal TLS probe implementation + ## v16.3.1 (date: 20.9.2023) - bug fix: extracting big template failed diff --git a/operations/tlscheck.go b/operations/tlscheck.go index 3afa6adc..e16fc367 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -8,11 +8,24 @@ import ( "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/settings" ) +type ( + tlsConfigs []*tls.Config +) + var ( - tlsVersions = map[uint16]string{} + tlsVersions = map[uint16]string{} + knownVersions = []uint16{ + tls.VersionTLS13, + tls.VersionTLS12, + tls.VersionTLS11, + tls.VersionTLS10, + tls.VersionSSL30, + } ) func init() { @@ -143,3 +156,93 @@ func tlsCheckHost(host string, roots map[string]bool) []*common.DiagnosticCheck } return result } + +func configurationVariations(root *x509.CertPool) tlsConfigs { + configs := make(tlsConfigs, len(knownVersions)) + for at, version := range knownVersions { + configs[at] = &tls.Config{ + InsecureSkipVerify: true, + RootCAs: root, + MinVersion: version, + MaxVersion: version, + } + } + return configs +} + +func certificateFingerprint(certificate *x509.Certificate) string { + if certificate == nil { + return "[nil]" + } + return fmt.Sprintf("[% 02X ...]", certificate.Signature[:10]) +} + +func probeVersion(serverport string, config *tls.Config, seen map[string]int) { + conn, err := tls.Dial("tcp", serverport, config) + if err != nil { + common.Log(" %s%s failed, reason: %v%s", pretty.Yellow, tlsVersions[config.MinVersion], err, pretty.Reset) + return + } + defer conn.Close() + state := conn.ConnectionState() + version, ok := tlsVersions[state.Version] + if !ok { + version = fmt.Sprintf("unknown: %03x", state.Version) + } + server := state.ServerName + toVerify := x509.VerifyOptions{ + DNSName: server, + Roots: config.RootCAs, + Intermediates: x509.NewCertPool(), + } + common.Log(" %s%s supported, server: %q%s", pretty.Green, version, server, pretty.Reset) + certificates := state.PeerCertificates + for at, certificate := range certificates { + if at > 0 { + toVerify.Intermediates.AddCert(certificate) + } + fingerprint := certificateFingerprint(certificate) + hit, ok := seen[fingerprint] + if ok { + common.Log(" %s#%d: [ID:%d] again %s%s", pretty.Grey, at, hit, fingerprint, pretty.Reset) + continue + } + hit = len(seen) + 1 + seen[fingerprint] = hit + names := strings.Join(certificate.DNSNames, ", ") + before := certificate.NotBefore.Format("2006-Jan-02") + after := certificate.NotAfter.Format("2006-Jan-02") + common.Log(" #%d: %s[ID:%d]%s %s %s - %s [%s]", at, pretty.Magenta, hit, pretty.Reset, fingerprint, before, after, names) + common.Log(" + subject %s", certificate.Subject) + common.Log(" + issuer %s", certificate.Issuer) + } + _, err = certificates[0].Verify(toVerify) + if err != nil { + common.Log(" %s!!! verification failure: %v%s", pretty.Red, err, pretty.Reset) + } +} + +func probeServer(index int, serverport string, variations tlsConfigs, seen map[string]int) { + common.Log("%s#%d: Server %q%s", pretty.Cyan, index, serverport, pretty.Reset) + for _, variation := range variations { + probeVersion(serverport, variation, seen) + } +} + +func TLSProbe(serverports []string) (err error) { + defer fail.Around(&err) + + root, err := x509.SystemCertPool() + fail.On(err != nil, "Cannot get system certificate pool, reason: %v", err) + + seen := make(map[string]int) + + variations := configurationVariations(root) + for at, serverport := range serverports { + if at > 0 { + common.Log("--") + } + probeServer(at+1, serverport, variations, seen) + } + return nil +} From 7d6b8d719e3575dadfa3485aa3749b58d3ea678a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 21 Sep 2023 09:38:32 +0300 Subject: [PATCH 433/516] Improvement: TLS probe (v16.4.1) --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/tlscheck.go | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 5ecfe607..e9b1724b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.4.0` + Version = `v16.4.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 19cffd81..fc3b74b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v16.4.1 (date: 21.9.2023) INTERNAL + +- improve: refining TLS probe (added cipher suite) + ## v16.4.0 (date: 21.9.2023) INTERNAL - feature: internal TLS probe implementation diff --git a/operations/tlscheck.go b/operations/tlscheck.go index e16fc367..3f436eca 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -185,6 +185,7 @@ func probeVersion(serverport string, config *tls.Config, seen map[string]int) { } defer conn.Close() state := conn.ConnectionState() + cipher := tls.CipherSuiteName(state.CipherSuite) version, ok := tlsVersions[state.Version] if !ok { version = fmt.Sprintf("unknown: %03x", state.Version) @@ -195,8 +196,9 @@ func probeVersion(serverport string, config *tls.Config, seen map[string]int) { Roots: config.RootCAs, Intermediates: x509.NewCertPool(), } - common.Log(" %s%s supported, server: %q%s", pretty.Green, version, server, pretty.Reset) + common.Log(" %s%s supported, cipher suite %q, server: %q%s", pretty.Green, version, cipher, server, pretty.Reset) certificates := state.PeerCertificates + before := len(seen) for at, certificate := range certificates { if at > 0 { toVerify.Intermediates.AddCert(certificate) @@ -216,6 +218,9 @@ func probeVersion(serverport string, config *tls.Config, seen map[string]int) { common.Log(" + subject %s", certificate.Subject) common.Log(" + issuer %s", certificate.Issuer) } + if len(seen) == before { + return + } _, err = certificates[0].Verify(toVerify) if err != nil { common.Log(" %s!!! verification failure: %v%s", pretty.Red, err, pretty.Reset) From 0c8b00a83a807f91b1a0aa1de7e6b1d20051f282 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 21 Sep 2023 13:28:26 +0300 Subject: [PATCH 434/516] Feature: new TLS variables (v16.5.0) - new variables set into environments: `RC_DISABLE_SSL`, `WDM_SSL_VERIFY`, `NODE_TLS_REJECT_UNAUTHORIZED`, and `RC_TLS_LEGACY_RENEGOTIATION_ALLOWED` - new settings option `legacy-renegotiation-allowed` - removed `automation-studio` from `autoupdates:` in settings.yaml file - settings.yaml version number updated to `2023.09` - added 5 second timeout to probe connections --- assets/settings.yaml | 4 ++-- common/version.go | 2 +- conda/robocorp.go | 6 ++++++ docs/changelog.md | 9 +++++++++ operations/tlscheck.go | 15 +++++++++++++-- settings/api.go | 1 + settings/data.go | 8 +++++--- settings/settings.go | 4 ++++ 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/assets/settings.yaml b/assets/settings.yaml index 9e399623..c52a35b5 100644 --- a/assets/settings.yaml +++ b/assets/settings.yaml @@ -18,7 +18,6 @@ diagnostics-hosts: autoupdates: assistant: https://downloads.robocorp.com/assistant/releases/ - automation-studio: https://downloads.robocorp.com/automation-studio/releases/ workforce-agent: https://downloads.robocorp.com/workforce-agent/releases/ setup-utility: https://downloads.robocorp.com/setup-utility/releases/ templates: https://downloads.robocorp.com/templates/templates.yaml @@ -26,6 +25,7 @@ autoupdates: certificates: verify-ssl: true ssl-no-revoke: false + legacy-renegotiation-allowed: false options: no-build: false @@ -42,4 +42,4 @@ meta: name: default description: default settings.yaml internal to rcc source: builtin - version: 2023.01 + version: 2023.09 diff --git a/common/version.go b/common/version.go index e9b1724b..a099aa94 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.4.1` + Version = `v16.5.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index c950ae11..33aaae7c 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -114,6 +114,12 @@ func injectNetworkEnvironment(environment []string) []string { } if !settings.Global.VerifySsl() { environment = append(environment, "MAMBA_SSL_VERIFY=false") + environment = append(environment, "RC_DISABLE_SSL=true") + environment = append(environment, "WDM_SSL_VERIFY=0") + environment = append(environment, "NODE_TLS_REJECT_UNAUTHORIZED=0") + } + if settings.Global.LegacyRenegotiation() { + environment = append(environment, "RC_TLS_LEGACY_RENEGOTIATION_ALLOWED=true") } environment = appendIfValue(environment, "https_proxy", settings.Global.HttpsProxy()) environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) diff --git a/docs/changelog.md b/docs/changelog.md index fc3b74b6..7018979c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v16.5.0 (date: 21.9.2023) + +- new variables set into environments: `RC_DISABLE_SSL`, `WDM_SSL_VERIFY`, + `NODE_TLS_REJECT_UNAUTHORIZED`, and `RC_TLS_LEGACY_RENEGOTIATION_ALLOWED` +- new settings option `legacy-renegotiation-allowed` +- removed `automation-studio` from `autoupdates:` in settings.yaml file +- settings.yaml version number updated to `2023.09` +- added 5 second timeout to probe connections + ## v16.4.1 (date: 21.9.2023) INTERNAL - improve: refining TLS probe (added cipher suite) diff --git a/operations/tlscheck.go b/operations/tlscheck.go index 3f436eca..acb18674 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -1,11 +1,13 @@ package operations import ( + "context" "crypto/tls" "crypto/x509" "fmt" "net/http" "strings" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" @@ -178,12 +180,21 @@ func certificateFingerprint(certificate *x509.Certificate) string { } func probeVersion(serverport string, config *tls.Config, seen map[string]int) { - conn, err := tls.Dial("tcp", serverport, config) + dialer := &tls.Dialer{ + Config: config, + } + timeout, _ := context.WithTimeout(context.Background(), 5*time.Second) + intermediate, err := dialer.DialContext(timeout, "tcp", serverport) if err != nil { common.Log(" %s%s failed, reason: %v%s", pretty.Yellow, tlsVersions[config.MinVersion], err, pretty.Reset) return } - defer conn.Close() + defer intermediate.Close() + conn, ok := intermediate.(*tls.Conn) + if !ok { + common.Log(" %s%s failed, reason: could not covert to TLS connection.%s", pretty.Yellow, tlsVersions[config.MinVersion], pretty.Reset) + return + } state := conn.ConnectionState() cipher := tls.CipherSuiteName(state.CipherSuite) version, ok := tlsVersions[state.Version] diff --git a/settings/api.go b/settings/api.go index d7f2dee1..73145532 100644 --- a/settings/api.go +++ b/settings/api.go @@ -34,5 +34,6 @@ type Api interface { HasCaBundle() bool VerifySsl() bool NoRevocation() bool + LegacyRenegotiation() bool NoBuid() bool } diff --git a/settings/data.go b/settings/data.go index 0d65686a..ba232557 100644 --- a/settings/data.go +++ b/settings/data.go @@ -231,9 +231,10 @@ func (it *Settings) Diagnostics(target *common.DiagnosticStatus) { } type Certificates struct { - VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` - SslNoRevoke bool `yaml:"ssl-no-revoke" json:"ssl-no-revoke"` - CaBundle string `yaml:"ca-bundle,omitempty" json:"ca-bundle,omitempty"` + VerifySsl bool `yaml:"verify-ssl" json:"verify-ssl"` + SslNoRevoke bool `yaml:"ssl-no-revoke" json:"ssl-no-revoke"` + LegacyRenegotiation bool `yaml:"legacy-renegotiation-allowed" json:"legacy-renegotiation-allowed"` + CaBundle string `yaml:"ca-bundle,omitempty" json:"ca-bundle,omitempty"` } func (it *Certificates) onTopOf(target *Settings) { @@ -242,6 +243,7 @@ func (it *Certificates) onTopOf(target *Settings) { } target.Certificates.VerifySsl = it.VerifySsl target.Certificates.SslNoRevoke = it.SslNoRevoke + target.Certificates.LegacyRenegotiation = it.LegacyRenegotiation if pathlib.IsFile(common.CaBundleFile()) { target.Certificates.CaBundle = common.CaBundleFile() } diff --git a/settings/settings.go b/settings/settings.go index 27908e4f..e0b61653 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -235,6 +235,10 @@ func (it gateway) VerifySsl() bool { return it.settings().Certificates.VerifySsl } +func (it gateway) LegacyRenegotiation() bool { + return it.settings().Certificates.LegacyRenegotiation +} + func (it gateway) NoRevocation() bool { return it.settings().Certificates.SslNoRevoke } From 2f802136afab66a8ed381cb37f98e63f0b842b43 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 22 Sep 2023 10:54:05 +0300 Subject: [PATCH 435/516] Feature: configuration TLS probe (v16.6.0) - internal probe becomes `rcc configuration tlsprobe` command - tlsprobe output improvements (address and DNS resolution) - sending metrics of `rcc.cli.run.failure` when automation exit code is something else than zero --- cmd/{internalProbe.go => configureTLSprobe.go} | 6 +++--- common/version.go | 2 +- docs/changelog.md | 7 +++++++ operations/running.go | 10 ++++++++-- operations/tlscheck.go | 17 +++++++++++++++-- 5 files changed, 34 insertions(+), 8 deletions(-) rename cmd/{internalProbe.go => configureTLSprobe.go} (86%) diff --git a/cmd/internalProbe.go b/cmd/configureTLSprobe.go similarity index 86% rename from cmd/internalProbe.go rename to cmd/configureTLSprobe.go index e9ee9092..305bac66 100644 --- a/cmd/internalProbe.go +++ b/cmd/configureTLSprobe.go @@ -20,8 +20,8 @@ func fixHosts(hosts []string) []string { return servers } -var internalProbeCmd = &cobra.Command{ - Use: "probe +", +var tlsProbeCmd = &cobra.Command{ + Use: "tlsprobe +", Short: "Probe host:port combinations for supported TLS versions.", Long: "Probe host:port combinations for supported TLS versions.", Run: func(cmd *cobra.Command, args []string) { @@ -33,5 +33,5 @@ var internalProbeCmd = &cobra.Command{ } func init() { - internalCmd.AddCommand(internalProbeCmd) + configureCmd.AddCommand(tlsProbeCmd) } diff --git a/common/version.go b/common/version.go index a099aa94..ca936a8f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.5.0` + Version = `v16.6.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7018979c..29a2452d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v16.6.0 (date: 22.9.2023) + +- internal probe becomes `rcc configuration tlsprobe` command +- tlsprobe output improvements (address and DNS resolution) +- sending metrics of `rcc.cli.run.failure` when automation exit code is + something else than zero + ## v16.5.0 (date: 21.9.2023) - new variables set into environments: `RC_DISABLE_SSL`, `WDM_SSL_VERIFY`, diff --git a/operations/running.go b/operations/running.go index 391c0dc2..9593ffd9 100644 --- a/operations/running.go +++ b/operations/running.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" @@ -354,10 +355,15 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro journal.CurrentBuildEvent().RobotStarts() pipe := WatchChildren(os.Getpid(), 200*time.Millisecond) shell.WithInterrupt(func() { + exitcode := 0 if common.NoOutputCapture { - _, err = shell.New(environment, directory, task...).Execute(interactive) + exitcode, err = shell.New(environment, directory, task...).Execute(interactive) } else { - _, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + exitcode, err = shell.New(environment, directory, task...).Tee(outputDir, interactive) + } + if exitcode != 0 { + details := fmt.Sprintf("%s_%d_%08x", common.Platform(), exitcode, uint32(exitcode)) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run.failure", details) } }) pretty.RccPointOfView(actualRun, err) diff --git a/operations/tlscheck.go b/operations/tlscheck.go index acb18674..f29136b6 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "net" "net/http" "strings" "time" @@ -179,6 +180,18 @@ func certificateFingerprint(certificate *x509.Certificate) string { return fmt.Sprintf("[% 02X ...]", certificate.Signature[:10]) } +func dnsLookup(serverport string) string { + parts := strings.Split(serverport, ":") + if len(parts) == 0 { + return "DNS: []" + } + dns, err := net.LookupHost(parts[0]) + if err != nil { + return fmt.Sprintf("DNS [%v]", err) + } + return fmt.Sprintf("DNS %q", dns) +} + func probeVersion(serverport string, config *tls.Config, seen map[string]int) { dialer := &tls.Dialer{ Config: config, @@ -207,7 +220,7 @@ func probeVersion(serverport string, config *tls.Config, seen map[string]int) { Roots: config.RootCAs, Intermediates: x509.NewCertPool(), } - common.Log(" %s%s supported, cipher suite %q, server: %q%s", pretty.Green, version, cipher, server, pretty.Reset) + common.Log(" %s%s supported, cipher suite %q, server: %q, address: %q%s", pretty.Green, version, cipher, server, conn.RemoteAddr(), pretty.Reset) certificates := state.PeerCertificates before := len(seen) for at, certificate := range certificates { @@ -239,7 +252,7 @@ func probeVersion(serverport string, config *tls.Config, seen map[string]int) { } func probeServer(index int, serverport string, variations tlsConfigs, seen map[string]int) { - common.Log("%s#%d: Server %q%s", pretty.Cyan, index, serverport, pretty.Reset) + common.Log("%s#%d: Server %q, %s%s", pretty.Cyan, index, serverport, dnsLookup(serverport), pretty.Reset) for _, variation := range variations { probeVersion(serverport, variation, seen) } From 666f54280fdf64b99a6b24707503b8f7813925ee Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 27 Sep 2023 08:31:29 +0300 Subject: [PATCH 436/516] Feature: profile removal (v16.7.0) - refactored profile commands into one file - added support for removing configuration profiles - updated robot tests to test profile removal - fix: added 3 second timeout to TLS checks --- cmd/configureexport.go | 36 ------- cmd/configureimport.go | 40 ------- cmd/configureprofile.go | 184 ++++++++++++++++++++++++++++++++ cmd/configureswitch.go | 102 ------------------ common/version.go | 2 +- docs/changelog.md | 7 ++ operations/diagnostics.go | 1 + operations/tlscheck.go | 5 +- robot_tests/documentation.robot | 1 - robot_tests/profile_beta.yaml | 1 + robot_tests/profiles.robot | 32 ++++-- 11 files changed, 224 insertions(+), 187 deletions(-) delete mode 100644 cmd/configureexport.go delete mode 100644 cmd/configureimport.go create mode 100644 cmd/configureprofile.go delete mode 100644 cmd/configureswitch.go diff --git a/cmd/configureexport.go b/cmd/configureexport.go deleted file mode 100644 index c7103e4b..00000000 --- a/cmd/configureexport.go +++ /dev/null @@ -1,36 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - configFile string - profileName string - clearProfile bool -) - -var configureExportCmd = &cobra.Command{ - Use: "export", - Short: "Export a configuration profile for Robocorp tooling.", - Long: "Export a configuration profile for Robocorp tooling.", - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Configuration export lasted").Report() - } - profile := loadNamedProfile(profileName) - err := profile.SaveAs(configFile) - pretty.Guard(err == nil, 1, "Error while exporting profile, reason: %v", err) - pretty.Ok() - }, -} - -func init() { - configureCmd.AddCommand(configureExportCmd) - configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename where configuration profile is exported.") - configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to export.") - configureExportCmd.MarkFlagRequired("profile") -} diff --git a/cmd/configureimport.go b/cmd/configureimport.go deleted file mode 100644 index 7c30fcaa..00000000 --- a/cmd/configureimport.go +++ /dev/null @@ -1,40 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/settings" - - "github.com/spf13/cobra" -) - -var ( - immediateSwitch bool -) - -var configureImportCmd = &cobra.Command{ - Use: "import", - Short: "Import a configuration profile for Robocorp tooling.", - Long: "Import a configuration profile for Robocorp tooling.", - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Configuration import lasted").Report() - } - profile := &settings.Profile{} - err := profile.LoadFrom(configFile) - pretty.Guard(err == nil, 1, "Error while loading profile: %v", err) - err = profile.Import() - pretty.Guard(err == nil, 2, "Error while importing profile: %v", err) - if immediateSwitch { - switchProfileTo(profile.Name) - } - pretty.Ok() - }, -} - -func init() { - configureCmd.AddCommand(configureImportCmd) - configureImportCmd.Flags().BoolVarP(&immediateSwitch, "switch", "s", false, "Immediately switch to use new profile.") - configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename to import as configuration profile.") - configureImportCmd.MarkFlagRequired("filename") -} diff --git a/cmd/configureprofile.go b/cmd/configureprofile.go new file mode 100644 index 00000000..c9960ceb --- /dev/null +++ b/cmd/configureprofile.go @@ -0,0 +1,184 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" + + "github.com/spf13/cobra" +) + +var ( + configFile string + profileName string + clearProfile bool + immediateSwitch bool +) + +func profileMap() map[string]string { + pattern := common.ExpandPath(filepath.Join(common.RobocorpHome(), "profile_*.yaml")) + found, err := filepath.Glob(pattern) + pretty.Guard(err == nil, 1, "Error while searching profiles: %v", err) + profiles := make(map[string]string) + for _, name := range found { + profile := settings.Profile{} + err = profile.LoadFrom(name) + if err == nil { + profiles[profile.Name] = profile.Description + } + } + return profiles +} + +func jsonListProfiles() { + profiles := make(map[string]interface{}) + profiles["profiles"] = profileMap() + profiles["current"] = settings.Global.Name() + content, err := operations.NiceJsonOutput(profiles) + pretty.Guard(err == nil, 1, "Error serializing profiles: %v", err) + common.Stdout("%s\n", content) +} + +func listProfiles() { + profiles := profileMap() + pretty.Guard(len(profiles) > 0, 2, "No profiles found, you must first import some.") + common.Stdout("Available profiles:\n") + for name, description := range profiles { + common.Stdout("- %s: %s\n", name, description) + } + common.Stdout("\n") +} + +func profileFullPath(name string) string { + filename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) + return common.ExpandPath(filepath.Join(common.RobocorpHome(), filename)) +} + +func loadNamedProfile(name string) *settings.Profile { + fullpath := profileFullPath(name) + profile := &settings.Profile{} + err := profile.LoadFrom(fullpath) + pretty.Guard(err == nil, 3, "Error while loading/parsing profile, reason: %v", err) + return profile +} + +func switchProfileTo(name string) { + profile := loadNamedProfile(name) + err := profile.Activate() + pretty.Guard(err == nil, 4, "Error while activating profile, reason: %v", err) +} + +func cleanupProfile() { + profile := settings.Profile{} + err := profile.Remove() + pretty.Guard(err == nil, 5, "Error while clearing profile, reason: %v", err) +} + +var configureSwitchCmd = &cobra.Command{ + Use: "switch", + Short: "Switch active configuration profile for Robocorp tooling.", + Long: "Switch active configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Configuration switch lasted").Report() + } + if clearProfile { + cleanupProfile() + pretty.Ok() + } else if len(profileName) == 0 { + if jsonFlag { + jsonListProfiles() + } else { + listProfiles() + common.Stdout("Currently active profile is: %s\n", settings.Global.Name()) + pretty.Ok() + } + } else { + switchProfileTo(profileName) + pretty.Ok() + } + }, +} + +var configureRemoveCmd = &cobra.Command{ + Use: "remove", + Short: "Remove named a configuration profile for Robocorp tooling.", + Long: "Remove named a configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Configuration remove lasted").Report() + } + profiles := profileMap() + description, ok := profiles[profileName] + pretty.Guard(ok, 2, "No match for profile with name %q.", profileName) + fullpath := profileFullPath(profileName) + + common.Log("Trying to remove profile: %s %q [%s].", profileName, description, fullpath) + err := os.Remove(fullpath) + pretty.Guard(err == nil, 5, "Error while removing profile file %q, reason: %v", fullpath, err) + + pretty.Ok() + }, +} + +var configureExportCmd = &cobra.Command{ + Use: "export", + Short: "Export a configuration profile for Robocorp tooling.", + Long: "Export a configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Configuration export lasted").Report() + } + profile := loadNamedProfile(profileName) + err := profile.SaveAs(configFile) + pretty.Guard(err == nil, 1, "Error while exporting profile, reason: %v", err) + pretty.Ok() + }, +} + +var configureImportCmd = &cobra.Command{ + Use: "import", + Short: "Import a configuration profile for Robocorp tooling.", + Long: "Import a configuration profile for Robocorp tooling.", + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag() { + defer common.Stopwatch("Configuration import lasted").Report() + } + profile := &settings.Profile{} + err := profile.LoadFrom(configFile) + pretty.Guard(err == nil, 1, "Error while loading profile: %v", err) + err = profile.Import() + pretty.Guard(err == nil, 2, "Error while importing profile: %v", err) + if immediateSwitch { + switchProfileTo(profile.Name) + } + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(configureSwitchCmd) + configureSwitchCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to activate.") + configureSwitchCmd.Flags().BoolVarP(&clearProfile, "noprofile", "n", false, "Remove active profile, and reset to defaults.") + configureSwitchCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show profile list as JSON stream.") + + configureCmd.AddCommand(configureRemoveCmd) + configureRemoveCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to remove.") + configureRemoveCmd.MarkFlagRequired("profile") + + configureCmd.AddCommand(configureExportCmd) + configureExportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename where configuration profile is exported.") + configureExportCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to export.") + configureExportCmd.MarkFlagRequired("profile") + + configureCmd.AddCommand(configureImportCmd) + configureImportCmd.Flags().BoolVarP(&immediateSwitch, "switch", "s", false, "Immediately switch to use new profile.") + configureImportCmd.Flags().StringVarP(&configFile, "filename", "f", "exported_profile.yaml", "The filename to import as configuration profile.") + configureImportCmd.MarkFlagRequired("filename") +} diff --git a/cmd/configureswitch.go b/cmd/configureswitch.go deleted file mode 100644 index f79813bb..00000000 --- a/cmd/configureswitch.go +++ /dev/null @@ -1,102 +0,0 @@ -package cmd - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/settings" - - "github.com/spf13/cobra" -) - -func profileMap() map[string]string { - pattern := common.ExpandPath(filepath.Join(common.RobocorpHome(), "profile_*.yaml")) - found, err := filepath.Glob(pattern) - pretty.Guard(err == nil, 1, "Error while searching profiles: %v", err) - profiles := make(map[string]string) - for _, name := range found { - profile := settings.Profile{} - err = profile.LoadFrom(name) - if err == nil { - profiles[profile.Name] = profile.Description - } - } - return profiles -} - -func jsonListProfiles() { - profiles := make(map[string]interface{}) - profiles["profiles"] = profileMap() - profiles["current"] = settings.Global.Name() - content, err := operations.NiceJsonOutput(profiles) - pretty.Guard(err == nil, 1, "Error serializing profiles: %v", err) - common.Stdout("%s\n", content) -} - -func listProfiles() { - profiles := profileMap() - pretty.Guard(len(profiles) > 0, 2, "No profiles found, you must first import some.") - common.Stdout("Available profiles:\n") - for name, description := range profiles { - common.Stdout("- %s: %s\n", name, description) - } - common.Stdout("\n") -} - -func loadNamedProfile(name string) *settings.Profile { - filename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) - fullpath := common.ExpandPath(filepath.Join(common.RobocorpHome(), filename)) - profile := &settings.Profile{} - err := profile.LoadFrom(fullpath) - pretty.Guard(err == nil, 3, "Error while loading/parsing profile, reason: %v", err) - return profile -} - -func switchProfileTo(name string) { - profile := loadNamedProfile(name) - err := profile.Activate() - pretty.Guard(err == nil, 4, "Error while activating profile, reason: %v", err) -} - -func cleanupProfile() { - profile := settings.Profile{} - err := profile.Remove() - pretty.Guard(err == nil, 5, "Error while clearing profile, reason: %v", err) -} - -var configureSwitchCmd = &cobra.Command{ - Use: "switch", - Short: "Switch active configuration profile for Robocorp tooling.", - Long: "Switch active configuration profile for Robocorp tooling.", - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag() { - defer common.Stopwatch("Configuration switch lasted").Report() - } - if clearProfile { - cleanupProfile() - pretty.Ok() - } else if len(profileName) == 0 { - if jsonFlag { - jsonListProfiles() - } else { - listProfiles() - common.Stdout("Currently active profile is: %s\n", settings.Global.Name()) - pretty.Ok() - } - } else { - switchProfileTo(profileName) - pretty.Ok() - } - }, -} - -func init() { - configureCmd.AddCommand(configureSwitchCmd) - configureSwitchCmd.Flags().StringVarP(&profileName, "profile", "p", "", "The name of configuration profile to activate.") - configureSwitchCmd.Flags().BoolVarP(&clearProfile, "noprofile", "n", false, "Remove active profile, and reset to defaults.") - configureSwitchCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show profile list as JSON stream.") -} diff --git a/common/version.go b/common/version.go index ca936a8f..c68060ce 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.6.0` + Version = `v16.7.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 29a2452d..88e9f517 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v16.7.0 (date: 27.9.2023) + +- refactored profile commands into one file +- added support for removing configuration profiles +- updated robot tests to test profile removal +- fix: added 3 second timeout to TLS checks + ## v16.6.0 (date: 22.9.2023) - internal probe becomes `rcc configuration tlsprobe` command diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 61538fe0..93ce518c 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -87,6 +87,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["config-http-proxy"] = settings.Global.HttpProxy() result.Details["config-ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) result.Details["config-ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) + result.Details["config-legacy-renegotiation-allowed"] = fmt.Sprintf("%v", settings.Global.LegacyRenegotiation()) result.Details["os-holo-location"] = common.HoloLocation() result.Details["hololib-location"] = common.HololibLocation() result.Details["hololib-catalog-location"] = common.HololibCatalogLocation() diff --git a/operations/tlscheck.go b/operations/tlscheck.go index f29136b6..c54dc010 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -49,7 +49,10 @@ func tlsCheckHeadOnly(url string) (*tls.ConnectionState, error) { // this is intentional, so that network diagnosis can detect // unsecure certificates, and connections to weaker TLS version // [ref: Github CodeQL security warning] - client := http.Client{Transport: transport} + client := http.Client{ + Transport: transport, + Timeout: 3 * time.Second, + } response, err := client.Head(url) if err != nil { return nil, err diff --git a/robot_tests/documentation.robot b/robot_tests/documentation.robot index 943efa40..38ba4603 100644 --- a/robot_tests/documentation.robot +++ b/robot_tests/documentation.robot @@ -1,7 +1,6 @@ *** Settings *** Resource resources.robot Test template Verify documentation -Default tags WIP *** Test cases *** DOCUMENTATION EXPECT diff --git a/robot_tests/profile_beta.yaml b/robot_tests/profile_beta.yaml index 7af7f5fe..e2910aa5 100644 --- a/robot_tests/profile_beta.yaml +++ b/robot_tests/profile_beta.yaml @@ -4,6 +4,7 @@ settings: certificates: verify-ssl: false ssl-no-revoke: true + legacy-renegotiation-allowed: true network: https-proxy: http://bad.betaputkinen.net:1234/ http-proxy: http://bad.betaputkinen.net:2345/ diff --git a/robot_tests/profiles.robot b/robot_tests/profiles.robot index e1d22675..44cc7085 100644 --- a/robot_tests/profiles.robot +++ b/robot_tests/profiles.robot @@ -2,6 +2,7 @@ Library OperatingSystem Library supporting.py Resource resources.robot +Default tags WIP *** Test cases *** @@ -21,8 +22,9 @@ Goal: Can import profiles into rcc Goal: Can see imported profiles Step build/rcc configuration switch - Must Have Alpha settings - Must Have Beta settings + Must Have Alpha: Alpha settings + Must Have Beta: Beta settings + Wont Have Gamma: Gamma settings Must Have Currently active profile is: default Use STDERR Must Have OK. @@ -46,8 +48,8 @@ Goal: Can switch to Alpha profile Use STDERR Must Have OK. -Goal: Diagnostics can show alpha profile information - Step build/rcc configuration diagnostics --json +Goal: Quick diagnostics can show alpha profile information + Step build/rcc configuration diagnostics --quick --json Must Be Json Response Must Have "config-micromambarc-used": "false" Must Have "config-piprc-used": "false" @@ -68,14 +70,15 @@ Goal: Can switch to Beta profile Use STDERR Must Have OK. -Goal: Diagnostics can show beta profile information - Step build/rcc configuration diagnostics --json +Goal: Quick diagnostics can show beta profile information + Step build/rcc configuration diagnostics --quick --json Must Be Json Response Must Have "config-micromambarc-used": "true" Must Have "config-piprc-used": "true" Must Have "config-settings-yaml-used": "true" Must Have "config-ssl-no-revoke": "true" Must Have "config-ssl-verify": "false" + Must Have "config-legacy-renegotiation-allowed": "true" Must Have "config-https-proxy": "http://bad.betaputkinen.net:1234/" Must Have "config-http-proxy": "http://bad.betaputkinen.net:2345/" @@ -86,6 +89,23 @@ Goal: Can import and switch to Gamma profile immediately Step build/rcc configuration switch Use STDOUT + Must Have Alpha: Alpha settings + Must Have Beta: Beta settings + Must Have Gamma: Gamma settings + Must Have Currently active profile is: Gamma + Use STDERR + Must Have OK. + +Goal: Can remove profile while it is still used + Step build/rcc configuration remove --profile Gamma + Use STDERR + Must Have OK. + + Step build/rcc configuration switch + Use STDOUT + Must Have Alpha: Alpha settings + Must Have Beta: Beta settings + Wont Have Gamma: Gamma settings Must Have Currently active profile is: Gamma Use STDERR Must Have OK. From 455b7899d2762904c5e416e7af1cbcc90e35007d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 29 Sep 2023 09:46:38 +0300 Subject: [PATCH 437/516] Bugfix: process tree blacklist (v16.7.1) - bugfix: added process blacklist to prevent old processes shown as child processes in process tree (also recycled PIDs will become "grey listed") and this bug was detected in Windows - improvement: changed command WaitDelay from 15 seconds to 3 seconds --- common/version.go | 2 +- docs/changelog.md | 7 +++++++ operations/processtree.go | 35 ++++++++++++++++++++++++++++++++--- shell/task.go | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index c68060ce..8692d3d0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.7.0` + Version = `v16.7.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 88e9f517..27e2ef41 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v16.7.1 (date: 29.9.2023) + +- bugfix: added process blacklist to prevent old processes shown as child + processes in process tree (also recycled PIDs will become "grey listed") + and this bug was detected in Windows +- improvement: changed command WaitDelay from 15 seconds to 3 seconds + ## v16.7.0 (date: 27.9.2023) - refactored profile commands into one file diff --git a/operations/processtree.go b/operations/processtree.go index 12504784..6bc4b1e5 100644 --- a/operations/processtree.go +++ b/operations/processtree.go @@ -18,15 +18,33 @@ type ( ProcessNode struct { Pid int Parent int + White bool Executable string Children ProcessMap } ) +var ( + processBlacklist = make(map[int]int) +) + +func init() { + processes, err := ProcessMapNow() + if err == nil { + rcc := os.Getpid() + for _, process := range processes { + if process.Pid != rcc { + processBlacklist[process.Pid] = process.Parent + } + } + } +} + func NewProcessNode(core ps.Process) *ProcessNode { return &ProcessNode{ Pid: core.Pid(), Parent: core.PPid(), + White: true, Executable: core.Executable(), Children: make(ProcessMap), } @@ -39,7 +57,13 @@ func ProcessMapNow() (ProcessMap, error) { } result := make(ProcessMap) for _, process := range processes { - result[process.Pid()] = NewProcessNode(process) + node := NewProcessNode(process) + old, ok := processBlacklist[node.Pid] + if ok && old == node.Parent { + continue + } + node.White = !ok + result[node.Pid] = node } for pid, process := range result { parent, ok := result[process.Parent] @@ -94,13 +118,17 @@ func (it *ProcessNode) warningTree(prefix string, newparent bool, limit int) { if len(it.Children) > 0 { kind = "container" } + var grey string + if !it.White { + grey = " (grey listed)" + } if newparent { kind = fmt.Sprintf("%s -> new parent PID: #%d", kind, it.Parent) } else { kind = fmt.Sprintf("%s under #%d", kind, it.Parent) } - pretty.Warning("%s#%d %q <%s>", prefix, it.Pid, it.Executable, kind) - common.RunJournal("orphan process", fmt.Sprintf("parent=%d pid=%d name=%s", it.Parent, it.Pid, it.Executable), "process pollution") + pretty.Warning("%s#%d %q <%s>%s%s", prefix, it.Pid, it.Executable, kind, pretty.Grey, grey) + common.RunJournal("orphan process", fmt.Sprintf("parent=%d pid=%d name=%s greylist=%v", it.Parent, it.Pid, it.Executable, !it.White), "process pollution") if limit < 0 { pretty.Warning("%s Maximum recursion depth detected. Truncating output here.", prefix) return @@ -192,6 +220,7 @@ func updateSeenChildren(pid int, processes ProcessMap, seen ChildMap) bool { } func WatchChildren(pid int, delay time.Duration) chan ChildMap { + common.Debug("Process blacklist size is %d processes.", len(processBlacklist)) pipe := make(chan ChildMap) go babySitter(pid, pipe, delay) return pipe diff --git a/shell/task.go b/shell/task.go index b3b65b15..fc9e72be 100644 --- a/shell/task.go +++ b/shell/task.go @@ -68,7 +68,7 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) command.Stdin = stdin command.Stdout = stdout command.Stderr = stderr - command.WaitDelay = 15 * time.Second + command.WaitDelay = 3 * time.Second err := command.Start() if err != nil { return -500, err From 78490d510be6f95c94804accc9dd00a4e3d8043f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 2 Oct 2023 11:06:36 +0300 Subject: [PATCH 438/516] Feature: settings.yaml age in diagnostics (v16.8.0) - improvement: quick diagnostics now has settings.yaml age visible as seconds - added `RCC_REMOTE_ORIGIN` variable to diagnostics output - deprecated interactive configuration, since Setup Utility should be used --- cmd/wizardconfig.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/diagnostics.go | 2 ++ pathlib/functions.go | 13 +++++++++++++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go index 2ada7106..5fe6e101 100644 --- a/cmd/wizardconfig.go +++ b/cmd/wizardconfig.go @@ -11,8 +11,8 @@ import ( var wizardConfigCommand = &cobra.Command{ Use: "configuration", Aliases: []string{"conf", "config", "configure"}, - Short: "Create a configuration profile for Robocorp tooling interactively.", - Long: "Create a configuration profile for Robocorp tooling interactively.", + Short: "Create a configuration profile for Robocorp tooling interactively. Deprecated. Use Setup Utility instead.", + Long: "Create a configuration profile for Robocorp tooling interactively. Deprecated. Use Setup Utility instead.", Run: func(cmd *cobra.Command, args []string) { if !pretty.Interactive { pretty.Exit(1, "This is for interactive use only. Do not use in scripting/CI!") diff --git a/common/version.go b/common/version.go index 8692d3d0..5ad3dfde 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.7.1` + Version = `v16.8.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 27e2ef41..81e1cb83 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v16.8.0 (date: 2.10.2023) + +- improvement: quick diagnostics now has settings.yaml age visible as seconds +- added `RCC_REMOTE_ORIGIN` variable to diagnostics output +- deprecated interactive configuration, since Setup Utility should be used + ## v16.7.1 (date: 29.9.2023) - bugfix: added process blacklist to prevent old processes shown as child diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 93ce518c..fea2f539 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -70,6 +70,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["ROBOCORP_HOME"] = common.RobocorpHome() result.Details["ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS"] = fmt.Sprintf("%v", common.OverrideSystemRequirements()) result.Details["RCC_VERBOSE_ENVIRONMENT_BUILDING"] = fmt.Sprintf("%v", common.VerboseEnvironmentBuilding()) + result.Details["RCC_REMOTE_ORIGIN"] = fmt.Sprintf("%v", common.RccRemoteOrigin()) result.Details["user-cache-dir"] = justText(os.UserCacheDir) result.Details["user-config-dir"] = justText(os.UserConfigDir) result.Details["user-home-dir"] = justText(os.UserHomeDir) @@ -82,6 +83,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["config-piprc-used"] = fmt.Sprintf("%v", settings.Global.HasPipRc()) result.Details["config-micromambarc-used"] = fmt.Sprintf("%v", settings.Global.HasMicroMambaRc()) result.Details["config-settings-yaml-used"] = fmt.Sprintf("%v", pathlib.IsFile(common.SettingsFile())) + result.Details["config-settings-yaml-age-seconds"] = fmt.Sprintf("%d", pathlib.Age(common.SettingsFile())) result.Details["config-active-profile"] = settings.Global.Name() result.Details["config-https-proxy"] = settings.Global.HttpsProxy() result.Details["config-http-proxy"] = settings.Global.HttpProxy() diff --git a/pathlib/functions.go b/pathlib/functions.go index d652ca90..b840a58d 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -53,6 +53,19 @@ func Exists(pathname string) bool { return !os.IsNotExist(err) } +func Age(pathname string) uint64 { + var milliseconds int64 + stat, err := os.Stat(pathname) + if !os.IsNotExist(err) { + milliseconds = time.Now().Sub(stat.ModTime()).Milliseconds() + } + seconds := milliseconds / 1000 + if seconds < 0 { + return 0 + } + return uint64(seconds) +} + func Abs(path string) (string, error) { if filepath.IsAbs(path) { return path, nil From 91ab5a3ac1357797ac7fabab739ad2a5bd5fa324 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 3 Oct 2023 15:03:22 +0300 Subject: [PATCH 439/516] Feature: --warranty-voided flag added (v16.9.0) - deterioration: added `--warranty-voided` mode to make system less robust but faster (do not use this mode, unless you really do know what you are doing) --- cloud/metrics.go | 3 +++ cmd/diagnostics.go | 2 +- cmd/holotreeVariables.go | 4 +++- cmd/rcc/main.go | 5 +++++ cmd/root.go | 6 ++++++ common/variables.go | 5 +++++ common/version.go | 2 +- conda/robocorp.go | 3 +++ docs/changelog.md | 5 +++++ htfs/commands.go | 9 +++++++++ htfs/library.go | 5 +++++ htfs/unmanaged.go | 4 ++++ htfs/virtual.go | 6 ++++++ htfs/ziplibrary.go | 6 ++++++ operations/cache.go | 3 +++ operations/diagnostics.go | 1 + pathlib/lock_unix.go | 2 +- pathlib/lock_windows.go | 2 +- xviper/tracking.go | 2 +- xviper/wrapper.go | 3 +++ 20 files changed, 72 insertions(+), 6 deletions(-) diff --git a/cloud/metrics.go b/cloud/metrics.go index f11a7a9f..e62e655f 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -42,6 +42,9 @@ func sendMetric(metricsHost, kind, name, value string) { } func BackgroundMetric(kind, name, value string) { + if common.WarrantyVoided() { + return + } metricsHost := settings.Global.TelemetryURL() if len(metricsHost) == 0 { return diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 7e4f4be2..a9492e92 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -23,7 +23,7 @@ var diagnosticsCmd = &cobra.Command{ if common.DebugFlag() { defer common.Stopwatch("Diagnostic run lasted").Report() } - _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag, quickFilterFlag) + _, err := operations.ProduceDiagnostics(fileOption, robotOption, jsonFlag, productionFlag, quickFilterFlag || common.WarrantyVoided()) if err != nil { pretty.Exit(1, "Error: %v", err) } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 5f615f21..22b18595 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -84,7 +84,9 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp holozip = config.Holozip() } path, _, err := htfs.NewEnvironment(condafile, holozip, true, force, operations.PullCatalog) - pretty.RccPointOfView(newEnvironment, err) + if !common.WarrantyVoided() { + pretty.RccPointOfView(newEnvironment, err) + } pretty.Guard(err == nil, 6, "%s", err) if Has(environment) { diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index ede34bb3..04f3ab4f 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -150,5 +150,10 @@ func main() { TimezoneMetric() } + if common.WarrantyVoided() { + common.Timeline("Running in 'warranty voided' mode.") + pretty.Warning("Note that 'rcc' is running in 'warranty voided' mode.") + } + anywork.Sync() } diff --git a/cmd/root.go b/cmd/root.go index e083d0dd..74e40ebc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -123,6 +123,7 @@ func init() { rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.LayeredHolotree, "layered", "", false, "use layered holotree spaces, experimental, DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", false, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") } func initConfig() { @@ -144,6 +145,11 @@ func initConfig() { common.UnifyStageHandling() pretty.Setup() + + if common.WarrantyVoided() { + pretty.Warning("Note that 'rcc' is running in 'warranty voided' mode.") + } + common.Timeline("%q", os.Args) common.Trace("CLI command was: %#v", os.Args) common.Debug("Using config file: %v", xviper.ConfigFileUsed()) diff --git a/common/variables.go b/common/variables.go index 60289866..fc5a38e4 100644 --- a/common/variables.go +++ b/common/variables.go @@ -46,6 +46,7 @@ var ( UnmanagedSpace bool FreshlyBuildEnvironment bool LayeredHolotree bool + WarrantyVoidedFlag bool StageFolder string ControllerType string HolotreeSpace string @@ -108,6 +109,10 @@ func RobocorpLock() string { return filepath.Join(RobocorpHome(), "robocorp.lck") } +func WarrantyVoided() bool { + return WarrantyVoidedFlag +} + func DebugFlag() bool { return verbosity >= Debugging } diff --git a/common/version.go b/common/version.go index 5ad3dfde..78315650 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.8.0` + Version = `v16.9.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 33aaae7c..17196709 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -125,6 +125,9 @@ func injectNetworkEnvironment(environment []string) []string { environment = appendIfValue(environment, "HTTPS_PROXY", settings.Global.HttpsProxy()) environment = appendIfValue(environment, "http_proxy", settings.Global.HttpProxy()) environment = appendIfValue(environment, "HTTP_PROXY", settings.Global.HttpProxy()) + if common.WarrantyVoided() { + environment = append(environment, "RCC_WARRANTY_VOIDED=true") + } return environment } diff --git a/docs/changelog.md b/docs/changelog.md index 81e1cb83..c3b6b49c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v16.9.0 (date: 3.10.2023) UNSTABLE + +- deterioration: added `--warranty-voided` mode to make system less robust but + faster (do not use this mode, unless you really do know what you are doing) + ## v16.8.0 (date: 2.10.2023) - improvement: quick diagnostics now has settings.yaml age visible as seconds diff --git a/htfs/commands.go b/htfs/commands.go index 15d696bb..afd131db 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -23,6 +23,14 @@ type CatalogPuller func(string, string, bool) error func NewEnvironment(condafile, holozip string, restore, force bool, puller CatalogPuller) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) + if common.WarrantyVoided() { + tree, err := New() + fail.On(err != nil, "%s", err) + + path := tree.WarrantyVoidedDir([]byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) + return path, common.NewScorecard(), err + } + journal.CurrentBuildEvent().StartNow(force) if settings.Global.NoBuild() { @@ -68,6 +76,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) + common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers on %d CPUs].", common.EnvironmentHash, common.Platform(), anywork.Scale(), runtime.NumCPU()) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) diff --git a/htfs/library.go b/htfs/library.go index 2e78fba7..719d15a1 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -55,6 +55,7 @@ type Library interface { ValidateBlueprint([]byte) error HasBlueprint([]byte) bool Open(string) (io.Reader, Closer, error) + WarrantyVoidedDir([]byte, []byte) string TargetDir([]byte, []byte, []byte) (string, error) Restore([]byte, []byte, []byte) (string, error) RestoreTo([]byte, string, string, string, bool) (string, error) @@ -326,6 +327,10 @@ func touchUsedHash(hash string) { pathlib.ForceTouchWhen(fullpath, pretty.ProgressMark) } +func (it *hololib) WarrantyVoidedDir(controller, space []byte) string { + return filepath.Join(common.HolotreeLocation(), ControllerSpaceName(controller, space)) +} + func (it *hololib) TargetDir(blueprint, controller, space []byte) (result string, err error) { defer fail.Around(&err) key := common.BlueprintHash(blueprint) diff --git a/htfs/unmanaged.go b/htfs/unmanaged.go index 728b9cf4..e594ed54 100644 --- a/htfs/unmanaged.go +++ b/htfs/unmanaged.go @@ -105,6 +105,10 @@ func (it *unmanaged) Record(blueprint []byte) error { return it.delegate.Record(blueprint) } +func (it *unmanaged) WarrantyVoidedDir(controller, space []byte) string { + return it.delegate.WarrantyVoidedDir(controller, space) +} + func (it *unmanaged) TargetDir(blueprint, client, tag []byte) (string, error) { return it.delegate.TargetDir(blueprint, client, tag) } diff --git a/htfs/virtual.go b/htfs/virtual.go index 2a1dafd0..1d2dbc22 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -10,6 +10,7 @@ import ( "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) type virtual struct { @@ -76,6 +77,11 @@ func (it *virtual) Record(blueprint []byte) (err error) { return nil } +func (it *virtual) WarrantyVoidedDir(controller, space []byte) string { + pretty.Exit(13, "hololib.zip does not support `--warranty-voided` running") + return "" +} + func (it *virtual) TargetDir(blueprint, client, tag []byte) (string, error) { name := ControllerSpaceName(client, tag) return filepath.Join(common.HolotreeLocation(), name), nil diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 58dde376..758582a6 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -13,6 +13,7 @@ import ( "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) type ziplibrary struct { @@ -78,6 +79,11 @@ func (it *ziplibrary) CatalogPath(key string) string { return filepath.Join("catalog", CatalogName(key)) } +func (it *ziplibrary) WarrantyVoidedDir(controller, space []byte) string { + pretty.Exit(13, "hololib.zip does not support `--warranty-voided` running") + return "" +} + func (it *ziplibrary) TargetDir(blueprint, client, tag []byte) (path string, err error) { defer fail.Around(&err) key := common.BlueprintHash(blueprint) diff --git a/operations/cache.go b/operations/cache.go index d072f648..7068e805 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -81,6 +81,9 @@ func SummonCache() (*Cache, error) { } func (it *Cache) Save() error { + if common.WarrantyVoided() { + return nil + } lockfile := cacheLockFile() completed := pathlib.LockWaitMessage(lockfile, "Serialized cache access [cache lock]") locker, err := pathlib.Locker(lockfile, 125) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index fea2f539..ea912e2f 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -106,6 +106,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["ENV:ComSpec"] = os.Getenv("ComSpec") result.Details["ENV:SHELL"] = os.Getenv("SHELL") result.Details["ENV:LANG"] = os.Getenv("LANG") + result.Details["warranty-voided-mode"] = fmt.Sprintf("%v", common.WarrantyVoided()) for name, filename := range lockfiles() { result.Details[name] = filename diff --git a/pathlib/lock_unix.go b/pathlib/lock_unix.go index c940e73a..f623481f 100644 --- a/pathlib/lock_unix.go +++ b/pathlib/lock_unix.go @@ -11,7 +11,7 @@ import ( ) func Locker(filename string, trycount int) (Releaser, error) { - if Lockless { + if common.WarrantyVoided() || Lockless { return Fake(), nil } if common.TraceFlag() { diff --git a/pathlib/lock_windows.go b/pathlib/lock_windows.go index 53ad1573..6027068b 100644 --- a/pathlib/lock_windows.go +++ b/pathlib/lock_windows.go @@ -29,7 +29,7 @@ type filehandle interface { } func Locker(filename string, trycount int) (Releaser, error) { - if Lockless { + if common.WarrantyVoided() || Lockless { return Fake(), nil } var file *os.File diff --git a/xviper/tracking.go b/xviper/tracking.go index c262c2db..2eb6158f 100644 --- a/xviper/tracking.go +++ b/xviper/tracking.go @@ -55,5 +55,5 @@ func ConsentTracking(state bool) { } func CanTrack() bool { - return GetBool(trackingConsentKey) + return GetBool(trackingConsentKey) && !common.WarrantyVoided() } diff --git a/xviper/wrapper.go b/xviper/wrapper.go index db5ab968..55e95123 100644 --- a/xviper/wrapper.go +++ b/xviper/wrapper.go @@ -35,6 +35,9 @@ type config struct { } func (it *config) Save() { + if common.WarrantyVoided() { + return + } if len(it.Filename) == 0 { return } From 4474c2e9cecc375047e056aa5a69bdea9f65e1de Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 4 Oct 2023 09:43:33 +0300 Subject: [PATCH 440/516] Breaking changes: layers and interactity (v17.0.0) - MAJOR breaking change: removed interactive configuration command, since Setup Utility now better covers that functionality - MAJOR breaking change: holotree is now layered by default and `--layered` option is gone - few documentation updates --- cmd/configureTLSprobe.go | 14 +++- cmd/root.go | 1 - cmd/wizardconfig.go | 33 -------- common/variables.go | 1 - common/version.go | 2 +- conda/workflows.go | 11 ++- docs/README.md | 2 +- docs/changelog.md | 8 ++ docs/profile_configuration.md | 14 +--- docs/troubleshooting.md | 2 + htfs/commands.go | 4 +- robot_tests/fullrun.robot | 2 +- settings/settings.go | 10 --- wizard/common.go | 14 ---- wizard/config.go | 154 ---------------------------------- 15 files changed, 39 insertions(+), 233 deletions(-) delete mode 100644 cmd/wizardconfig.go delete mode 100644 wizard/config.go diff --git a/cmd/configureTLSprobe.go b/cmd/configureTLSprobe.go index 305bac66..3e4840cf 100644 --- a/cmd/configureTLSprobe.go +++ b/cmd/configureTLSprobe.go @@ -23,7 +23,19 @@ func fixHosts(hosts []string) []string { var tlsProbeCmd = &cobra.Command{ Use: "tlsprobe +", Short: "Probe host:port combinations for supported TLS versions.", - Long: "Probe host:port combinations for supported TLS versions.", + Long: `Probe host:port combinations for supported TLS versions. + +This command will show following information on your TLS settings: +- current DNS resolution give host +- which TLS versions are available on specific host:port combo +- server name, address, port, and cipher suite that actually was negotiated +- certificate chains that was seen on that connection + +Examples: + rcc configuration tlsprobe www.bing.com www.google.com + rcc configuration tlsprobe outlook.office365.com:993 outlook.office365.com:995 + rcc configuration tlsprobe api.us1.robocorp.com api.eu1.robocorp.com +`, Run: func(cmd *cobra.Command, args []string) { servers := fixHosts(args) err := operations.TLSProbe(servers) diff --git a/cmd/root.go b/cmd/root.go index 74e40ebc..c7c40fdc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -122,7 +122,6 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.StrictFlag, "strict", "", false, "be more strict on environment creation and handling") rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") - rootCmd.PersistentFlags().BoolVarP(&common.LayeredHolotree, "layered", "", false, "use layered holotree spaces, experimental, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", false, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") } diff --git a/cmd/wizardconfig.go b/cmd/wizardconfig.go deleted file mode 100644 index 5fe6e101..00000000 --- a/cmd/wizardconfig.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/wizard" - - "github.com/spf13/cobra" -) - -var wizardConfigCommand = &cobra.Command{ - Use: "configuration", - Aliases: []string{"conf", "config", "configure"}, - Short: "Create a configuration profile for Robocorp tooling interactively. Deprecated. Use Setup Utility instead.", - Long: "Create a configuration profile for Robocorp tooling interactively. Deprecated. Use Setup Utility instead.", - Run: func(cmd *cobra.Command, args []string) { - if !pretty.Interactive { - pretty.Exit(1, "This is for interactive use only. Do not use in scripting/CI!") - } - if common.DebugFlag() { - defer common.Stopwatch("Interactive configuration lasted").Report() - } - err := wizard.Configure(args) - if err != nil { - pretty.Exit(2, "%v", err) - } - pretty.Ok() - }, -} - -func init() { - interactiveCmd.AddCommand(wizardConfigCommand) -} diff --git a/common/variables.go b/common/variables.go index fc5a38e4..707ed5a2 100644 --- a/common/variables.go +++ b/common/variables.go @@ -45,7 +45,6 @@ var ( Liveonly bool UnmanagedSpace bool FreshlyBuildEnvironment bool - LayeredHolotree bool WarrantyVoidedFlag bool StageFolder string ControllerType string diff --git a/common/version.go b/common/version.go index 78315650..37f1238e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v16.9.0` + Version = `v17.0.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 091249f8..c3f1f735 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -301,28 +301,30 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t if !success { return success, fatal, false, "" } - if common.LayeredHolotree && (pipNeeded || postInstall) { - fmt.Fprintf(theplan, "\n--- micromamba layer complete [on layerd holotree] ---\n\n") + if pipNeeded || postInstall { + fmt.Fprintf(theplan, "\n--- micromamba layer complete [on layered holotree] ---\n\n") common.Error("saving rcc_plan.log", theplan.Save()) common.Error("saving golden master", goldenMaster(targetFolder, false)) recorder.Record([]byte(layers[0])) } } else { pretty.Progress(7, "Skipping micromamba phase, layer exists.") + fmt.Fprintf(planWriter, "\n--- micromamba plan skipped, layer exists ---\n\n") } if skip < SkipPipLayer { success, fatal, pipUsed, python = pipLayer(fingerprints[1], requirementsText, targetFolder, stopwatch, planWriter) if !success { return success, fatal, pipUsed, python } - if common.LayeredHolotree && pipUsed && postInstall { - fmt.Fprintf(theplan, "\n--- pip layer complete [on layerd holotree] ---\n\n") + if pipUsed && postInstall { + fmt.Fprintf(theplan, "\n--- pip layer complete [on layered holotree] ---\n\n") common.Error("saving rcc_plan.log", theplan.Save()) common.Error("saving golden master", goldenMaster(targetFolder, true)) recorder.Record([]byte(layers[1])) } } else { pretty.Progress(8, "Skipping pip phase, layer exists.") + fmt.Fprintf(planWriter, "\n--- pip plan skiped, layer exists ---\n\n") } if skip < SkipPostinstallLayer { success, fatal = postInstallLayer(fingerprints[2], finalEnv.PostInstall, targetFolder, stopwatch, planWriter) @@ -331,6 +333,7 @@ func holotreeLayers(condaYaml, requirementsText string, finalEnv *Environment, t } } else { pretty.Progress(9, "Skipping post install scripts phase, layer exists.") + fmt.Fprintf(planWriter, "\n--- post install plan skipped, layer exists ---\n\n") } return true, false, pipUsed, python } diff --git a/docs/README.md b/docs/README.md index 25428d49..7793eb30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -78,7 +78,7 @@ #### 4.1.2 [What does it contain?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-does-it-contain) ### 4.2 [Quick start guide](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#quick-start-guide) #### 4.2.1 [Setup Utility -- user interface for this](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#setup-utility----user-interface-for-this) -#### 4.2.2 [Pure rcc workflow](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#pure-rcc-workflow) +#### 4.2.2 [Pure rcc workflow for handling existing profiles](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#pure-rcc-workflow-for-handling-existing-profiles) ### 4.3 [What is needed?](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#what-is-needed) ### 4.4 [Discovery process](https://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md#discovery-process) ### 4.5 [What is execution environment isolation and caching?](https://github.com/robocorp/rcc/blob/master/docs/environment-caching.md#what-is-execution-environment-isolation-and-caching) diff --git a/docs/changelog.md b/docs/changelog.md index c3b6b49c..7bb1237c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v17.0.0 (date: 4.10.2023) UNSTABLE + +- MAJOR breaking change: removed interactive configuration command, since + Setup Utility now better covers that functionality +- MAJOR breaking change: holotree is now layered by default and `--layered` + option is gone +- few documentation updates + ## v16.9.0 (date: 3.10.2023) UNSTABLE - deterioration: added `--warranty-voided` mode to make system less robust but diff --git a/docs/profile_configuration.md b/docs/profile_configuration.md index c83ab2b9..0937b060 100644 --- a/docs/profile_configuration.md +++ b/docs/profile_configuration.md @@ -28,12 +28,9 @@ can be active at any moment. More behind [this link](https://robocorp.com/docs/control-room/setup-utility). -### Pure rcc workflow +### Pure rcc workflow for handling existing profiles ```sh -# interactively create "Office" profile -rcc interactive configuration Office - # import that Office profile, so that it can be used rcc configuration import --filename profile_office.yaml @@ -66,9 +63,6 @@ rcc configuration export --profile Office --filename shared.yaml ## Discovery process 1. You must be inside that network that you are targetting the configuration. -2. Run interactive configuration and answer questions there. -3. Take created profile in use. -4. Run diagnostics and speed test to verify functionality. -5. Repeat these steps until everything works. -6. Export profile and share it with rest of your team/organization. -7. Create other profiles for different network locations (remote, VPN, ...) +2. Run Setup Utility and use it to setup and verify your profile. +3. Export profile and share it with rest of your team/organization. +4. Create other profiles for different network locations (remote, VPN, ...) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1aae6220..bbe1317f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -6,6 +6,8 @@ - run command `rcc configuration diagnostics` and see if there are warnings, failures or errors in output (and same with `rcc configuration netdiagnostics`) +- run command `rcc configuration tlsprobe` against various host:port targets + to get insights about supported TLS versions, and certificates used there - if failure is with specific robot, then try running command `rcc configuration diagnostics --robot path/to/robot.yaml` and see if those robot diagnostics have something that identifies a problem (or to get diff --git a/htfs/commands.go b/htfs/commands.go index afd131db..199ea3c2 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -173,11 +173,11 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool, scorec fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) skip := conda.SkipNoLayers - if !force && common.LayeredHolotree { + if !force { pretty.Progress(6, "Restore partial environment into holotree stage %q.", tree.Stage()) skip = RestoreLayersTo(tree, identityfile, tree.Stage()) } else { - pretty.Progress(6, "Restore partial environment skipped. Layers disabled or force used.") + pretty.Progress(6, "Restore partial environment skipped. Force used.") } err = os.WriteFile(identityfile, blueprint, 0o644) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index fe820f3c..5e354c91 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -12,7 +12,7 @@ Fullrun setup Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v16. + Must Have v17. Goal: Show rcc license information. Step build/rcc man license --controller citests diff --git a/settings/settings.go b/settings/settings.go index e0b61653..329a2917 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -71,16 +71,6 @@ func LoadSetting(filename string) (*Settings, error) { return config, nil } -func TemporalSettingsLayer(filename string) error { - config, err := LoadSetting(filename) - if err != nil { - return err - } - chain[2] = config - cachedSettings = nil - return nil -} - func SummonSettings() (*Settings, error) { if cachedSettings != nil { return cachedSettings, nil diff --git a/wizard/common.go b/wizard/common.go index 1daf117f..ec2237be 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" ) @@ -47,19 +46,6 @@ func regexpValidation(validator *regexp.Regexp, erratic string) Validator { } } -func optionalFileValidation(erratic string) Validator { - return func(input string) bool { - if len(strings.TrimSpace(input)) == 0 { - return true - } - if !pathlib.IsFile(input) { - common.Stdout("%s%s%s\n\n", pretty.Red, erratic, pretty.Reset) - return false - } - return true - } -} - func warning(condition bool, message string) { if condition { common.Stdout("%s%s%s\n\n", pretty.Yellow, message, pretty.Reset) diff --git a/wizard/config.go b/wizard/config.go deleted file mode 100644 index ccc7c136..00000000 --- a/wizard/config.go +++ /dev/null @@ -1,154 +0,0 @@ -package wizard - -import ( - "fmt" - "os" - "regexp" - "strings" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/settings" -) - -var ( - proxyPattern = regexp.MustCompile("^(?:http[\\S]*)?$") - anyPattern = regexp.MustCompile("^\\s*\\S+") -) - -type question struct { - Identity string - Question string - Validator Validator -} - -type questions []question - -type answers map[string]string - -func questionaire(questions questions, answers answers) error { - for _, question := range questions { - previous := answers[question.Identity] - indirect, ok := answers[previous] - if ok { - previous = indirect - } - answer, err := ask(question.Question, previous, question.Validator) - if err != nil { - return err - } - answers[question.Identity] = answer - } - return nil -} - -func Configure(arguments []string) error { - common.Stdout("\n") - - note("Read documentation at %shttps://github.com/robocorp/rcc/blob/master/docs/profile_configuration.md%s\n", pretty.Green, pretty.White) - note("You are now configuring a profile to be used in Robocorp toolchain.\n") - note("If you want to clear some value, try giving just one space as a value.") - note("If you want to use default value, just press enter.\n") - - answers := make(answers) - - warning(len(arguments) > 1, "You provided more than one argument, but only the first one will be\nused as the name.") - - filename, err := ask("Path to (optional) settings.yaml", "", optionalFileValidation("Value should be valid file in filesystem.")) - if err != nil { - return err - } - if len(filename) > 0 { - settings.TemporalSettingsLayer(filename) - answers["settings-yaml"] = filename - } - - answers["profile-name"] = firstOf(arguments, settings.Global.Name()) - answers["profile-description"] = settings.Global.Description() - answers["https-proxy"] = settings.Global.HttpsProxy() - answers["http-proxy"] = settings.Global.HttpProxy() - answers["ssl-verify"] = fmt.Sprintf("%v", settings.Global.VerifySsl()) - answers["ssl-no-revoke"] = fmt.Sprintf("%v", settings.Global.NoRevocation()) - - err = questionaire(questions{ - {"profile-name", "Give profile a name", regexpValidation(namePattern, "Use just normal english word characters and no spaces!")}, - {"profile-description", "Give a short description for this profile", regexpValidation(anyPattern, "Description cannot be empty!")}, - {"https-proxy", "URL for https proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, - {"http-proxy", "URL for http proxy", regexpValidation(proxyPattern, "Must be empty or start with 'http' and should not contain spaces!")}, - {"ssl-verify", "Verify SSL certificated (ssl-verify)", memberValidation([]string{"true", "false"}, "Must be either true or false")}, - {"ssl-no-revoke", "Do not check SSL revocations (ssl-no-revoke)", memberValidation([]string{"true", "false"}, "Must be either true or false")}, - {"micromamba-rc", "Optional path to micromambarc file", optionalFileValidation("Value should be valid file in filesystem.")}, - {"pip-rc", "Optional path to piprc/pip.ini file", optionalFileValidation("Value should be valid file in filesystem.")}, - {"ca-bundle", "Optional path to CA bundle [pem format] file", optionalFileValidation("Value should be valid file in filesystem.")}, - }, answers) - if err != nil { - return err - } - - name := answers["profile-name"] - profile := &settings.Profile{ - Name: name, - Description: answers["profile-description"], - } - - blob, ok := pullFile(answers["settings-yaml"]) - if ok { - profile.Settings, _ = settings.FromBytes(blob) - } else { - profile.Settings = settings.Empty() - } - - profile.Settings.Network = &settings.Network{ - HttpsProxy: answers["https-proxy"], - HttpProxy: answers["http-proxy"], - } - - profile.Settings.Certificates = &settings.Certificates{ - VerifySsl: answers["ssl-verify"] == "true", - SslNoRevoke: answers["ssl-no-revoke"] == "true", - } - - profile.Settings.Meta.Name = name - profile.Settings.Meta.Description = answers["profile-description"] - - blob, ok = pullFile(answers["micromamba-rc"]) - if ok { - profile.MicroMambaRc = string(blob) - } - - blob, ok = pullFile(answers["pip-rc"]) - if ok { - profile.PipRc = string(blob) - } - - blob, ok = pullFile(answers["ca-bundle"]) - if ok { - profile.CaBundle = string(blob) - } - - profilename := fmt.Sprintf("profile_%s.yaml", strings.ToLower(name)) - profile.SaveAs(profilename) - - note("Saved profile into file %q.\n", profilename) - note("Next steps:") - note(" 1. command `%srcc configuration import --filename %s%s` imports created profile.", pretty.Grey, profilename, pretty.White) - note(" 2. command `%srcc configuration switch --profile %s%s` switches to that profile.", pretty.Grey, name, pretty.White) - note(" 3. command `%srcc configuration diagnostics%s` shows diagnostics with that configuration.", pretty.Grey, pretty.White) - note(" 4. command `%srcc configuration speedtest%s` can verify full environment creation.", pretty.Grey, pretty.White) - note(" 5. command `%srcc interactive configuration %s%s` runs this interactive command again.", pretty.Grey, name, pretty.White) - note(" 6. command `%srcc configuration switch --noprofile%s` inactivates all profiles.\n", pretty.Grey, pretty.White) - - return nil -} - -func pullFile(filename string) ([]byte, bool) { - if !pathlib.IsFile(filename) { - return nil, false - } - body, err := os.ReadFile(filename) - if err != nil { - return nil, false - } - return body, true -} From e0b343b16adf0ce31e211dfeff92fd7872d91c00 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 10 Oct 2023 14:29:43 +0300 Subject: [PATCH 441/516] Warranty voided improvements (v17.0.1) - early detection of `--warranty-voided` flag to allow init usage - more functionality skipped when "warranty voided", so that rcc is more read-only with that flag --- common/variables.go | 7 ++++++- common/version.go | 2 +- docs/changelog.md | 6 ++++++ htfs/directory.go | 12 +++++++----- journal/journal.go | 3 +++ pathlib/functions.go | 9 ++++++++- robot_tests/fullrun.robot | 27 +++++++++++++++++++++++++++ 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/common/variables.go b/common/variables.go index 707ed5a2..4c856d7c 100644 --- a/common/variables.go +++ b/common/variables.go @@ -8,6 +8,8 @@ import ( "runtime" "strings" "time" + + "github.com/robocorp/rcc/set" ) type ( @@ -64,6 +66,9 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) + // peek CLI options to pre-initialize "Warranty Voided" indicator + WarrantyVoidedFlag = set.Member(set.Set(os.Args), "--warranty-voided") + // Note: HololibCatalogLocation, HololibLibraryLocation and HololibUsageLocation // are force created from "htfs" direcotry.go init function // Also: HolotreeLocation creation is left for actual holotree commands @@ -337,7 +342,7 @@ func isDir(pathname string) bool { } func ensureDirectory(name string) { - if !isDir(name) { + if !WarrantyVoided() && !isDir(name) { Error("mkdir", os.MkdirAll(name, 0o750)) } } diff --git a/common/version.go b/common/version.go index 37f1238e..2f20f8e2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.0.0` + Version = `v17.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7bb1237c..feffafdf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.0.1 (date: 10.10.2023) UNSTABLE + +- early detection of `--warranty-voided` flag to allow init usage +- more functionality skipped when "warranty voided", so that rcc is more + read-only with that flag + ## v17.0.0 (date: 4.10.2023) UNSTABLE - MAJOR breaking change: removed interactive configuration command, since diff --git a/htfs/directory.go b/htfs/directory.go index 03d76cf8..9e970d8a 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -32,11 +32,13 @@ func init() { killfile[".svn"] = true killfile[".gitignore"] = true - pathlib.MakeSharedDir(common.HoloLocation()) - pathlib.MakeSharedDir(common.HololibCatalogLocation()) - pathlib.MakeSharedDir(common.HololibLibraryLocation()) - pathlib.MakeSharedDir(common.HololibUsageLocation()) - pathlib.MakeSharedDir(common.HololibPids()) + if !common.WarrantyVoided() { + pathlib.MakeSharedDir(common.HoloLocation()) + pathlib.MakeSharedDir(common.HololibCatalogLocation()) + pathlib.MakeSharedDir(common.HololibLibraryLocation()) + pathlib.MakeSharedDir(common.HololibUsageLocation()) + pathlib.MakeSharedDir(common.HololibPids()) + } } type ( diff --git a/journal/journal.go b/journal/journal.go index dc0ec16b..fe0a4564 100644 --- a/journal/journal.go +++ b/journal/journal.go @@ -88,6 +88,9 @@ func Post(event, detail, commentForm string, fields ...interface{}) (err error) func appendJournal(journalname string, blob []byte) (err error) { defer fail.Around(&err) + if common.WarrantyVoided() { + return nil + } handle, err := os.OpenFile(journalname, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) fail.On(err != nil, "Failed to open event journal %v -> %v", journalname, err) defer handle.Close() diff --git a/pathlib/functions.go b/pathlib/functions.go index b840a58d..ef02ea99 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -35,6 +35,9 @@ func Create(filename string) (*os.File, error) { } func WriteFile(filename string, data []byte, mode os.FileMode) error { + if common.WarrantyVoided() { + return nil + } _, err := EnsureParentDirectory(filename) if err != nil { return fmt.Errorf("Failed to ensure that parent directories for %q exist, reason: %v", filename, err) @@ -233,6 +236,10 @@ func ensureCorrectMode(fullpath string, stat fs.FileInfo, correct fs.FileMode) ( func makeModedDir(fullpath string, correct fs.FileMode) (path string, err error) { defer fail.Around(&err) + if common.WarrantyVoided() { + return fullpath, nil + } + stat, err := os.Stat(fullpath) if err == nil && stat.IsDir() { return ensureCorrectMode(fullpath, stat, correct) @@ -274,7 +281,7 @@ func doEnsureDirectory(directory string, mode fs.FileMode) (string, error) { if err != nil { return "", err } - if IsDir(fullpath) { + if common.WarrantyVoided() || IsDir(fullpath) { return fullpath, nil } err = os.MkdirAll(fullpath, mode) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 5e354c91..756658fd 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -181,6 +181,33 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have ROBOT_ARTIFACTS= Step build/rcc holotree check --controller citests +Goal: See variables from specific environment with warranty voided + Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml --warranty-voided + Must Have ROBOCORP_HOME= + Must Have PYTHON_EXE= + Must Have RCC_EXE= + Must Have CONDA_DEFAULT_ENV=rcc + Must Have CONDA_PREFIX= + Must Have CONDA_PROMPT_MODIFIER=(rcc) + Must Have CONDA_SHLVL=1 + Must Have PATH= + Must Have PYTHONHOME= + Must Have PYTHONEXECUTABLE= + Must Have PYTHONNOUSERSITE=1 + Must Have TEMP= + Must Have TMP= + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have RCC_TRACKING_ALLOWED= + Must Have PYTHONPATH= + Must Have ROBOT_ROOT= + Must Have ROBOT_ARTIFACTS= + Use STDERR + Wont Have Progress: 01/15 + Wont Have Progress: 02/15 + Wont Have Progress: 15/15 + Must Have Warning: Note that 'rcc' is running in 'warranty voided' mode. + Goal: See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc holotree variables --controller citests --json conda/testdata/conda.yaml Must Be Json Response From b9fb6a29fa5a1c52440aedb5d1f711ad041dff6e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 10 Oct 2023 16:25:50 +0300 Subject: [PATCH 442/516] Operating system info (v17.1.0) - operating system infomation on diagnostics and progress items --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 5 +++++ operations/diagnostics.go | 1 + settings/platform.go | 41 ++++++++++++++++++++++++++++++++++++ settings/platform_darwin.go | 5 +++++ settings/platform_linux.go | 5 +++++ settings/platform_windows.go | 5 +++++ 8 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 settings/platform.go create mode 100644 settings/platform_darwin.go create mode 100644 settings/platform_linux.go create mode 100644 settings/platform_windows.go diff --git a/common/version.go b/common/version.go index 2f20f8e2..b57fe74d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.0.1` + Version = `v17.1.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index feffafdf..a5edd90c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.1.0 (date: 10.10.2023) UNSTABLE + +- operating system infomation on diagnostics and progress items + ## v17.0.1 (date: 10.10.2023) UNSTABLE - early detection of `--warranty-voided` flag to allow init usage diff --git a/htfs/commands.go b/htfs/commands.go index 199ea3c2..23a4ac48 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -2,6 +2,7 @@ package htfs import ( "os" + "os/user" "path/filepath" "runtime" "strings" @@ -23,6 +24,10 @@ type CatalogPuller func(string, string, bool) error func NewEnvironment(condafile, holozip string, restore, force bool, puller CatalogPuller) (label string, scorecard common.Scorecard, err error) { defer fail.Around(&err) + who, _ := user.Current() + host, _ := os.Hostname() + pretty.Progress(0, "Context: %q <%v@%v> [%v/%v].", who.Name, who.Username, host, common.Platform(), settings.OperatingSystem()) + if common.WarrantyVoided() { tree, err := New() fail.On(err != nil, "%s", err) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index ea912e2f..5ced3f3a 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -99,6 +99,7 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["holotree-global-shared"] = fmt.Sprintf("%v", pathlib.IsFile(common.SharedMarkerLocation())) result.Details["holotree-user-id"] = common.UserHomeIdentity() result.Details["os"] = common.Platform() + result.Details["os-details"] = settings.OperatingSystem() result.Details["cpus"] = fmt.Sprintf("%d", runtime.NumCPU()) result.Details["when"] = time.Now().Format(time.RFC3339 + " (MST)") result.Details["timezone"] = time.Now().Format("MST") diff --git a/settings/platform.go b/settings/platform.go new file mode 100644 index 00000000..1e116934 --- /dev/null +++ b/settings/platform.go @@ -0,0 +1,41 @@ +package settings + +import ( + "regexp" + "strings" + + "github.com/robocorp/rcc/shell" +) + +const ( + liningPattern = `\r?\n` + spacingPattern = `\s+` +) + +var ( + spacingForm = regexp.MustCompile(spacingPattern) + liningForm = regexp.MustCompile(liningPattern) +) + +func operatingSystem() string { + output, _, err := shell.New(nil, ".", osInfoCommand...).CaptureOutput() + if err != nil { + output = err.Error() + } + return output +} + +func pickLines(text string) []string { + result := []string{} + for _, part := range liningForm.Split(text, -1) { + flat := strings.TrimSpace(strings.Join(spacingForm.Split(part, -1), " ")) + if len(flat) > 0 { + result = append(result, flat) + } + } + return result +} + +func OperatingSystem() string { + return strings.Join(pickLines(operatingSystem()), "; ") +} diff --git a/settings/platform_darwin.go b/settings/platform_darwin.go new file mode 100644 index 00000000..4d51b2ac --- /dev/null +++ b/settings/platform_darwin.go @@ -0,0 +1,5 @@ +package settings + +var ( + osInfoCommand = []string{"sw_vers"} +) diff --git a/settings/platform_linux.go b/settings/platform_linux.go new file mode 100644 index 00000000..c3830db4 --- /dev/null +++ b/settings/platform_linux.go @@ -0,0 +1,5 @@ +package settings + +var ( + osInfoCommand = []string{"lsb_release", "-a"} +) diff --git a/settings/platform_windows.go b/settings/platform_windows.go new file mode 100644 index 00000000..56d56f47 --- /dev/null +++ b/settings/platform_windows.go @@ -0,0 +1,5 @@ +package settings + +var ( + osInfoCommand = []string{"ver"} +) From 13e5c8f51bd21e4e6f62ea180d12c0a92186ac03 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 11 Oct 2023 09:26:46 +0300 Subject: [PATCH 443/516] Fix: Windows operating system info (v17.1.1) - bugfix: operating system information executed differently in windows - added hostname and user name to diagnostic information --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 4 ++++ settings/platform_windows.go | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index b57fe74d..b6bc8690 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.1.0` + Version = `v17.1.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index a5edd90c..5d98591f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.1.1 (date: 11.10.2023) UNSTABLE + +- bugfix: operating system information executed differently in windows +- added hostname and user name to diagnostic information + ## v17.1.0 (date: 10.10.2023) UNSTABLE - operating system infomation on diagnostics and progress items diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 5ced3f3a..3009a4a7 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -71,10 +71,14 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS"] = fmt.Sprintf("%v", common.OverrideSystemRequirements()) result.Details["RCC_VERBOSE_ENVIRONMENT_BUILDING"] = fmt.Sprintf("%v", common.VerboseEnvironmentBuilding()) result.Details["RCC_REMOTE_ORIGIN"] = fmt.Sprintf("%v", common.RccRemoteOrigin()) + who, _ := user.Current() + result.Details["user-name"] = who.Name + result.Details["user-username"] = who.Username result.Details["user-cache-dir"] = justText(os.UserCacheDir) result.Details["user-config-dir"] = justText(os.UserConfigDir) result.Details["user-home-dir"] = justText(os.UserHomeDir) result.Details["working-dir"] = justText(os.Getwd) + result.Details["hostname"] = justText(os.Hostname) result.Details["tempdir"] = os.TempDir() result.Details["controller"] = common.ControllerIdentity() result.Details["user-agent"] = common.UserAgent() diff --git a/settings/platform_windows.go b/settings/platform_windows.go index 56d56f47..04c967c3 100644 --- a/settings/platform_windows.go +++ b/settings/platform_windows.go @@ -1,5 +1,5 @@ package settings var ( - osInfoCommand = []string{"ver"} + osInfoCommand = []string{"cmd.exe", "/c", "ver"} ) From d4ecb16b500afc6ae302b820a29b10642b53dc1b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 11 Oct 2023 11:43:11 +0300 Subject: [PATCH 444/516] Fix: Windows micromamba activation bug (v17.1.2) - bugfix: Windows micromamba activation failures - bugfix: operating system information was leaking process STDERR - added operating system information to speed test output --- cmd/speed.go | 2 ++ common/version.go | 2 +- conda/platform_windows.go | 2 +- docs/changelog.md | 6 ++++++ settings/platform.go | 2 +- shell/task.go | 13 ++++++++++++- 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/cmd/speed.go b/cmd/speed.go index a1d9d5a9..ba11b4df 100644 --- a/cmd/speed.go +++ b/cmd/speed.go @@ -15,6 +15,7 @@ import ( "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -58,6 +59,7 @@ var speedtestCmd = &cobra.Command{ if common.DebugFlag() { defer common.Stopwatch("Speed test run lasted").Report() } + common.Log("System %q running on %q.", settings.OperatingSystem(), common.Platform()) common.Log("Running network and filesystem performance tests with %d workers on %d CPUs.", anywork.Scale(), runtime.NumCPU()) common.Log("This may take several minutes, please be patient.") signal := make(chan bool) diff --git a/common/version.go b/common/version.go index b6bc8690..6cbfc52e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.1.1` + Version = `v17.1.2` ) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 77309a8c..d6aa6959 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -23,7 +23,7 @@ const ( binSuffix = "\\bin" activateScript = "@echo off\n" + "set \"MAMBA_ROOT_PREFIX={{.MambaRootPrefix}}\"\n" + - "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Robocorphome}}\\bin\\micromamba.exe\" shell activate -s cmd.exe -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + + "for /f \"tokens=* usebackq\" %%a in ( `call \"{{.Micromamba}}\" shell activate -s cmd.exe -p \"{{.Live}}\"` ) do ( call \"%%a\" )\n" + "call \"{{.Rcc}}\" internal env -l after\n" commandSuffix = ".cmd" ) diff --git a/docs/changelog.md b/docs/changelog.md index 5d98591f..262a0a81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.1.2 (date: 11.10.2023) + +- bugfix: Windows micromamba activation failures +- bugfix: operating system information was leaking process STDERR +- added operating system information to speed test output + ## v17.1.1 (date: 11.10.2023) UNSTABLE - bugfix: operating system information executed differently in windows diff --git a/settings/platform.go b/settings/platform.go index 1e116934..c6b0e5f7 100644 --- a/settings/platform.go +++ b/settings/platform.go @@ -18,7 +18,7 @@ var ( ) func operatingSystem() string { - output, _, err := shell.New(nil, ".", osInfoCommand...).CaptureOutput() + output, _, err := shell.New(nil, ".", osInfoCommand...).NoStderr().CaptureOutput() if err != nil { output = err.Error() } diff --git a/shell/task.go b/shell/task.go index fc9e72be..e5907390 100644 --- a/shell/task.go +++ b/shell/task.go @@ -28,6 +28,7 @@ type ( executable string args []string stderronly bool + nostderr bool } Wrapper func() @@ -45,6 +46,7 @@ func New(environment []string, directory string, task ...string) *Task { executable: executable, args: args, stderronly: false, + nostderr: false, } } @@ -53,6 +55,11 @@ func (it *Task) StderrOnly() *Task { return it } +func (it *Task) NoStderr() *Task { + it.nostderr = true + return it +} + func (it *Task) stdout() io.Writer { if it.stderronly { return os.Stderr @@ -67,7 +74,11 @@ func (it *Task) execute(stdin io.Reader, stdout, stderr io.Writer) (int, error) command.Dir = it.directory command.Stdin = stdin command.Stdout = stdout - command.Stderr = stderr + if it.nostderr { + command.Stderr = nil + } else { + command.Stderr = stderr + } command.WaitDelay = 3 * time.Second err := command.Start() if err != nil { From f9876e7759f10048a855450e738fc9969b17024b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 12 Oct 2023 08:20:20 +0300 Subject: [PATCH 445/516] Fix: used environment configuration visibility (v17.1.3) - fix: made used environment configuration visible on progress entry and also noted once on first unique contact --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 2 +- pretty/functions.go | 11 ++++++++++- robot/robot.go | 6 ++++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 6cbfc52e..019d51e5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.1.2` + Version = `v17.1.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 262a0a81..7bddcd07 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.1.3 (date: 12.10.2023) + +- fix: made used environment configuration visible on progress entry and + also noted once on first unique contact + ## v17.1.2 (date: 11.10.2023) - bugfix: Windows micromamba activation failures diff --git a/htfs/commands.go b/htfs/commands.go index 23a4ac48..cc859b83 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -83,7 +83,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal fail.On(err != nil, "%s", err) common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false - pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers on %d CPUs].", common.EnvironmentHash, common.Platform(), anywork.Scale(), runtime.NumCPU()) + pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers on %d CPUs from %q].", common.EnvironmentHash, common.Platform(), anywork.Scale(), runtime.NumCPU(), filepath.Base(condafile)) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() diff --git a/pretty/functions.go b/pretty/functions.go index 2d94415c..7e877406 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -14,7 +14,8 @@ const ( ) var ( - ProgressMark time.Time + ProgressMark time.Time + onlyOnceMessages = make(map[string]bool) ) func init() { @@ -26,6 +27,14 @@ func Ok() error { return nil } +func JustOnce(format string, rest ...interface{}) { + message := fmt.Sprintf(format, rest...) + if !onlyOnceMessages[message] { + onlyOnceMessages[message] = true + Highlight(format, rest...) + } +} + func DebugNote(format string, rest ...interface{}) { niceform := fmt.Sprintf("%s%sNote: %s%s", Blue, Bold, format, Reset) common.Debug(niceform, rest...) diff --git a/robot/robot.go b/robot/robot.go index a9d27794..d993be0b 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -375,6 +375,12 @@ func (it *robot) UsesConda() bool { } func (it *robot) CondaConfigFile() string { + resolved := it.resolveCondaConfigFile() + pretty.JustOnce("Note! For now, resolved effective environment configuration file is %q.", resolved) + return resolved +} + +func (it *robot) resolveCondaConfigFile() string { available := it.availableEnvironmentConfigurations(common.Platform()) if len(available) > 0 { return available[0] From c0d7fdd2cc26ead85d79208138fb2203819804ad Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 12 Oct 2023 13:47:26 +0300 Subject: [PATCH 446/516] Upgrade: micromamba v1.5.1 (v17.2.0) - micromamba upgrade to v1.5.1 --- common/version.go | 2 +- conda/robocorp.go | 4 ++-- docs/changelog.md | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 019d51e5..e92d8a96 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.1.3` + Version = `v17.2.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 17196709..4af5c3f1 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -19,8 +19,8 @@ import ( const ( // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_004_009 - MicromambaVersionNumber = "v1.4.9" + MicromambaVersionLimit = 1_005_001 + MicromambaVersionNumber = "v1.5.1" ) var ( diff --git a/docs/changelog.md b/docs/changelog.md index 7bddcd07..f69b6f20 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.2.0 (date: 12.10.2023) + +- micromamba upgrade to v1.5.1 + ## v17.1.3 (date: 12.10.2023) - fix: made used environment configuration visible on progress entry and From 5e25b4141ff7a14b719d3daa96bf656fcccae7f2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 16 Oct 2023 14:34:31 +0300 Subject: [PATCH 447/516] Feature: embed micromamba (v17.3.0) - embedded micromamba inside rcc executable - removed micromamba download support since it extract all the way - removing support for arm64 architectures (linux, mac, windows) since embedded micromamba is not available on those architectures --- Rakefile | 41 ++++++++++------------ blobs/asset_test.go | 1 + blobs/assets/.gitignore | 1 + blobs/embedded.go | 8 +++++ blobs/micromamba_darwin.go | 10 ++++++ blobs/micromamba_linux.go | 10 ++++++ blobs/micromamba_windows.go | 10 ++++++ common/platform_darwin.go | 5 +++ common/platform_linux.go | 5 +++ common/platform_windows.go | 5 +++ common/version.go | 2 +- conda/installing.go | 68 ++++++++++++++++++++++++------------- docs/changelog.md | 7 ++++ 13 files changed, 125 insertions(+), 48 deletions(-) create mode 100644 blobs/micromamba_darwin.go create mode 100644 blobs/micromamba_linux.go create mode 100644 blobs/micromamba_windows.go diff --git a/Rakefile b/Rakefile index 086c9076..c703153f 100644 --- a/Rakefile +++ b/Rakefile @@ -21,13 +21,29 @@ task :tooling do end task :noassets do + rm_f FileList['blobs/assets/micromamba.*'] rm_f FileList['blobs/assets/*.zip'] rm_f FileList['blobs/assets/*.yaml'] rm_f FileList['blobs/assets/man/*.txt'] rm_f FileList['blobs/docs/*.md'] end -task :assets => [:noassets] do +def download_link(version, platform, filename) + "https://downloads.robocorp.com/micromamba/#{version}/#{platform}/#{filename}" +end + +task :micromamba do + version = 'v1.5.1' + url = download_link(version, "macos64", "micromamba") + sh "curl -o blobs/assets/micromamba.darwin_amd64 #{url}" + url = download_link(version, "windows64", "micromamba.exe") + sh "curl -o blobs/assets/micromamba.windows_amd64 #{url}" + url = download_link(version, "linux64", "micromamba") + sh "curl -o blobs/assets/micromamba.linux_amd64 #{url}" + sh "gzip -9 blobs/assets/micromamba.*" +end + +task :assets => [:noassets, :micromamba] do FileList['templates/*/'].each do |directory| basename = File.basename(directory) assetname = File.absolute_path(File.join("blobs", "assets", "#{basename}.zip")) @@ -61,13 +77,6 @@ task :linux64 => [:what, :test] do sh "sha256sum build/linux64/* || true" end -task :linux64arm => [:what, :test] do - ENV['GOOS'] = 'linux' - ENV['GOARCH'] = 'arm64' - sh "go build -ldflags '-s' -o build/linux64/arm/ ./cmd/..." - sh "sha256sum build/linux64/arm/* || true" -end - task :macos64 => [:support] do ENV['GOOS'] = 'darwin' ENV['GOARCH'] = 'amd64' @@ -75,13 +84,6 @@ task :macos64 => [:support] do sh "sha256sum build/macos64/* || true" end -task :macos64arm => [:support] do - ENV['GOOS'] = 'darwin' - ENV['GOARCH'] = 'arm64' - sh "go build -ldflags '-s' -o build/macos64/arm/ ./cmd/..." - sh "sha256sum build/macos64/arm/* || true" -end - task :windows64 => [:support] do ENV['GOOS'] = 'windows' ENV['GOARCH'] = 'amd64' @@ -89,13 +91,6 @@ task :windows64 => [:support] do sh "sha256sum build/windows64/* || true" end -task :windows64arm => [:support] do - ENV['GOOS'] = 'windows' - ENV['GOARCH'] = 'arm64' - sh "go build -ldflags '-s' -o build/windows64/arm/ ./cmd/..." - sh "sha256sum build/windows64/arm/* || true" -end - desc 'Setup build environment' task :robotsetup do sh "#{PYTHON} -m pip install --upgrade -r robot_requirements.txt" @@ -113,7 +108,7 @@ task :robot => :local do end desc 'Build commands to linux, macos, and windows.' -task :build => [:tooling, :version_txt, :linux64, :linux64arm, :macos64, :macos64arm, :windows64, :windows64arm] do +task :build => [:tooling, :version_txt, :linux64, :macos64, :windows64] do sh 'ls -l $(find build -type f)' end diff --git a/blobs/asset_test.go b/blobs/asset_test.go index 5f961dda..54b39806 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -43,6 +43,7 @@ func TestCanOtherAssets(t *testing.T) { wont_be.Panic(func() { blobs.MustAsset("docs/profile_configuration.md") }) wont_be.Panic(func() { blobs.MustAsset("docs/recipes.md") }) wont_be.Panic(func() { blobs.MustAsset("docs/usecases.md") }) + wont_be.Panic(func() { blobs.MustMicromamba() }) } func TestCanGetTemplateNamesThruOperations(t *testing.T) { diff --git a/blobs/assets/.gitignore b/blobs/assets/.gitignore index c5df735e..1da4801c 100644 --- a/blobs/assets/.gitignore +++ b/blobs/assets/.gitignore @@ -1,2 +1,3 @@ *.zip *.yaml +micromamba.* diff --git a/blobs/embedded.go b/blobs/embedded.go index 0a76adaf..140b2ad8 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -19,3 +19,11 @@ func MustAsset(name string) []byte { } return body } + +func MustMicromamba() []byte { + body, err := micromamba.ReadFile(micromambaName) + if err != nil { + panic(err) + } + return body +} diff --git a/blobs/micromamba_darwin.go b/blobs/micromamba_darwin.go new file mode 100644 index 00000000..7b0abd4c --- /dev/null +++ b/blobs/micromamba_darwin.go @@ -0,0 +1,10 @@ +package blobs + +import ( + "embed" +) + +//go:embed assets/micromamba.darwin_amd64.gz +var micromamba embed.FS + +var micromambaName = "assets/micromamba.darwin_amd64.gz" diff --git a/blobs/micromamba_linux.go b/blobs/micromamba_linux.go new file mode 100644 index 00000000..44bf0fc3 --- /dev/null +++ b/blobs/micromamba_linux.go @@ -0,0 +1,10 @@ +package blobs + +import ( + "embed" +) + +//go:embed assets/micromamba.linux_amd64.gz +var micromamba embed.FS + +var micromambaName = "assets/micromamba.linux_amd64.gz" diff --git a/blobs/micromamba_windows.go b/blobs/micromamba_windows.go new file mode 100644 index 00000000..3a55328d --- /dev/null +++ b/blobs/micromamba_windows.go @@ -0,0 +1,10 @@ +package blobs + +import ( + "embed" +) + +//go:embed assets/micromamba.windows_amd64.gz +var micromamba embed.FS + +var micromambaName = "assets/micromamba.windows_amd64.gz" diff --git a/common/platform_darwin.go b/common/platform_darwin.go index 0b3ec4b8..0e2bb2b3 100644 --- a/common/platform_darwin.go +++ b/common/platform_darwin.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" ) const ( @@ -28,3 +29,7 @@ func GenerateKillCommand(keys []int) string { } return strings.Join(command, " ") } + +func PlatformSyncDelay() { + time.Sleep(3 * time.Millisecond) +} diff --git a/common/platform_linux.go b/common/platform_linux.go index 0af598ca..392a8cc1 100644 --- a/common/platform_linux.go +++ b/common/platform_linux.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" ) const ( @@ -28,3 +29,7 @@ func GenerateKillCommand(keys []int) string { } return strings.Join(command, " ") } + +func PlatformSyncDelay() { + time.Sleep(3 * time.Millisecond) +} diff --git a/common/platform_windows.go b/common/platform_windows.go index 47109bf4..ef5d856d 100644 --- a/common/platform_windows.go +++ b/common/platform_windows.go @@ -6,6 +6,7 @@ import ( "path/filepath" "regexp" "strings" + "time" ) const ( @@ -46,3 +47,7 @@ func GenerateKillCommand(keys []int) string { } return strings.Join(command, " ") } + +func PlatformSyncDelay() { + time.Sleep(300 * time.Millisecond) +} diff --git a/common/version.go b/common/version.go index e92d8a96..a51f74f2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.2.0` + Version = `v17.3.0` ) diff --git a/conda/installing.go b/conda/installing.go index b95c9bd4..261e83e5 100644 --- a/conda/installing.go +++ b/conda/installing.go @@ -1,48 +1,68 @@ package conda import ( + "bytes" + "compress/gzip" + "io" "os" "time" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) func MustMicromamba() bool { - return HasMicroMamba() || ((DoDownload(1*time.Millisecond) || DoDownload(1*time.Second) || DoDownload(3*time.Second)) && DoInstall()) + return HasMicroMamba() || DoExtract(1*time.Millisecond) || DoExtract(1*time.Second) || DoExtract(3*time.Second) || DoFailMicromamba() } -func DoDownload(delay time.Duration) bool { - if common.DebugFlag() { - defer common.Stopwatch("Download done in").Report() - } +func DoFailMicromamba() bool { + pretty.Exit(113, "Could not extract micromamba, see above stream for more details.") + return false +} - common.Log("Downloading micromamba, this may take awhile ...") +func GunzipWrite(context, filename string, blob []byte) (err error) { + defer fail.Around(&err) - time.Sleep(delay) + stream := bytes.NewReader(blob) + source, err := gzip.NewReader(stream) + fail.On(err != nil, "Failed to %q -> %v", filename, err) - err := cloud.Download(MicromambaLink(), BinMicromamba()) - if err != nil { - common.Fatal("Download", err) - os.Remove(BinMicromamba()) - return false - } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.download", common.Version) - return true -} + sink, err := pathlib.Create(filename) + fail.On(err != nil, "Failed to create %q reader -> %v", context, err) + defer sink.Close() -func DoInstall() bool { - if common.DebugFlag() { - defer common.Stopwatch("Installation done in").Report() - } + _, err = io.Copy(sink, source) + fail.On(err != nil, "Failed to copy %q to %q -> %v", context, filename, err) + + err = sink.Sync() + fail.On(err != nil, "Failed to sync %q -> %v", filename, err) + + return nil +} - common.Log("Making micromamba executable ...") +func DoExtract(delay time.Duration) bool { + pretty.Highlight("Note: Extracting micromamba binary from inside rcc.") - err := os.Chmod(BinMicromamba(), 0o755) + time.Sleep(delay) + binary := blobs.MustMicromamba() + err := GunzipWrite("micromamba", BinMicromamba(), binary) + if err != nil { + err = os.Remove(BinMicromamba()) + if err != nil { + common.Fatal("Remove of micromamba failed, reason:", err) + } + return false + } + err = os.Chmod(BinMicromamba(), 0o755) if err != nil { - common.Fatal("Install", err) + common.Fatal("Could not make micromamba executalbe, reason:", err) return false } - cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.install", common.Version) + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.micromamba.extract", common.Version) + common.PlatformSyncDelay() return true } diff --git a/docs/changelog.md b/docs/changelog.md index f69b6f20..ec31ac15 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.3.0 (date: 16.10.2023) WORK IN PROGRESS + +- embedded micromamba inside rcc executable +- removed micromamba download support since it extract all the way +- removing support for arm64 architectures (linux, mac, windows) since + embedded micromamba is not available on those architectures + ## v17.2.0 (date: 12.10.2023) - micromamba upgrade to v1.5.1 From b2303cdb18a1362998bf0a488c7fc9a0668e5606 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 18 Oct 2023 08:57:44 +0300 Subject: [PATCH 448/516] Fix: micromamba version as asset (v17.3.1) - minor fix: now used micromamba version number is stored in separate asset file, to keep things in sync between build scripts and rcc binary --- Rakefile | 6 ++++-- assets/micromamba_version.txt | 1 + blobs/assets/.gitignore | 2 +- blobs/embedded.go | 16 ++++++++++++++++ common/version.go | 2 +- conda/platform_darwin.go | 3 ++- conda/platform_linux.go | 3 ++- conda/platform_windows.go | 3 ++- conda/robocorp.go | 11 +++-------- conda/robocorp_test.go | 5 +++-- docs/changelog.md | 5 +++++ 11 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 assets/micromamba_version.txt diff --git a/Rakefile b/Rakefile index c703153f..8f3532b4 100644 --- a/Rakefile +++ b/Rakefile @@ -33,14 +33,15 @@ def download_link(version, platform, filename) end task :micromamba do - version = 'v1.5.1' + version = File.read('assets/micromamba_version.txt').strip() + puts "Using micromamba version #{version}" url = download_link(version, "macos64", "micromamba") sh "curl -o blobs/assets/micromamba.darwin_amd64 #{url}" url = download_link(version, "windows64", "micromamba.exe") sh "curl -o blobs/assets/micromamba.windows_amd64 #{url}" url = download_link(version, "linux64", "micromamba") sh "curl -o blobs/assets/micromamba.linux_amd64 #{url}" - sh "gzip -9 blobs/assets/micromamba.*" + sh "gzip -f -9 blobs/assets/micromamba.*" end task :assets => [:noassets, :micromamba] do @@ -51,6 +52,7 @@ task :assets => [:noassets, :micromamba] do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end + cp FileList['assets/micromamba_version.txt'], 'blobs/assets/' cp FileList['assets/*.yaml'], 'blobs/assets/' cp FileList['assets/man/*.txt'], 'blobs/assets/man/' cp FileList['docs/*.md'], 'blobs/docs/' diff --git a/assets/micromamba_version.txt b/assets/micromamba_version.txt new file mode 100644 index 00000000..c9b3c015 --- /dev/null +++ b/assets/micromamba_version.txt @@ -0,0 +1 @@ +v1.5.1 \ No newline at end of file diff --git a/blobs/assets/.gitignore b/blobs/assets/.gitignore index 1da4801c..64e6f319 100644 --- a/blobs/assets/.gitignore +++ b/blobs/assets/.gitignore @@ -1,3 +1,3 @@ *.zip *.yaml -micromamba.* +micromamba* diff --git a/blobs/embedded.go b/blobs/embedded.go index 140b2ad8..1eb5ef10 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -2,10 +2,18 @@ package blobs import ( "embed" + "strings" +) + +const ( + // for micromamba upgrade, change following constants to match + // and also remember to update assets/micromamba_version.txt to match this + MicromambaVersionLimit = 1_005_001 ) //go:embed assets/*.yaml docs/*.md //go:embed assets/*.zip assets/man/*.txt +//go:embed assets/micromamba_version.txt var content embed.FS func Asset(name string) ([]byte, error) { @@ -27,3 +35,11 @@ func MustMicromamba() []byte { } return body } + +func MicromambaVersion() string { + body, err := Asset("assets/micromamba_version.txt") + if err != nil { + return "v0.0.0" + } + return strings.TrimSpace(string(body)) +} diff --git a/common/version.go b/common/version.go index a51f74f2..fd58bc54 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.3.0` + Version = `v17.3.1` ) diff --git a/conda/platform_darwin.go b/conda/platform_darwin.go index 83e79207..7659d182 100644 --- a/conda/platform_darwin.go +++ b/conda/platform_darwin.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -38,7 +39,7 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), blobs.MicromambaVersion())) err := pathlib.EnsureDirectoryExists(location) if err != nil { pretty.Warning("Problem creating %q, reason: %v", location, err) diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 6db517ec..54134a45 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -42,7 +43,7 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), blobs.MicromambaVersion())) err := pathlib.EnsureDirectoryExists(location) if err != nil { pretty.Warning("Problem creating %q, reason: %v", location, err) diff --git a/conda/platform_windows.go b/conda/platform_windows.go index d6aa6959..01ee1f5b 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -7,6 +7,7 @@ import ( "golang.org/x/sys/windows/registry" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -47,7 +48,7 @@ func CondaEnvironment() []string { } func BinMicromamba() string { - location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), MicromambaVersionNumber)) + location := common.ExpandPath(filepath.Join(common.MicromambaLocation(), blobs.MicromambaVersion())) err := pathlib.EnsureDirectoryExists(location) if err != nil { pretty.Warning("Problem creating %q, reason: %v", location, err) diff --git a/conda/robocorp.go b/conda/robocorp.go index 4af5c3f1..5e19e994 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/settings" @@ -17,12 +18,6 @@ import ( "github.com/robocorp/rcc/xviper" ) -const ( - // for micromamba upgrade, change following constants to match - MicromambaVersionLimit = 1_005_001 - MicromambaVersionNumber = "v1.5.1" -) - var ( ignoredPaths = []string{ "python", @@ -38,7 +33,7 @@ var ( ) func micromambaLink(platform, filename string) string { - return fmt.Sprintf("micromamba/%s/%s/%s", MicromambaVersionNumber, platform, filename) + return fmt.Sprintf("micromamba/%s/%s/%s", blobs.MicromambaVersion(), platform, filename) } func sorted(files []os.FileInfo) { @@ -241,7 +236,7 @@ func HasMicroMamba() bool { return false } version, versionText := AsVersion(MicromambaVersion()) - goodEnough := version >= MicromambaVersionLimit + goodEnough := version >= blobs.MicromambaVersionLimit common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/conda/robocorp_test.go b/conda/robocorp_test.go index f18a21c7..cd5618fa 100644 --- a/conda/robocorp_test.go +++ b/conda/robocorp_test.go @@ -3,6 +3,7 @@ package conda_test import ( "testing" + "github.com/robocorp/rcc/blobs" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/hamlet" ) @@ -31,6 +32,6 @@ func TestCanParsePipVersion(t *testing.T) { func TestInternalMicromambaVersionConsistency(t *testing.T) { must_be, _ := hamlet.Specifications(t) - needs, _ := conda.AsVersion(conda.MicromambaVersionNumber) - must_be.Equal(uint64(conda.MicromambaVersionLimit), needs) + needs, _ := conda.AsVersion(blobs.MicromambaVersion()) + must_be.Equal(uint64(blobs.MicromambaVersionLimit), needs) } diff --git a/docs/changelog.md b/docs/changelog.md index ec31ac15..d474bbb9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.3.1 (date: 18.10.2023) + +- minor fix: now used micromamba version number is stored in separate asset + file, to keep things in sync between build scripts and rcc binary + ## v17.3.0 (date: 16.10.2023) WORK IN PROGRESS - embedded micromamba inside rcc executable From fa3b120ea7c4c1d0c978fb6a295e4027d8137899 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 23 Oct 2023 09:42:48 +0300 Subject: [PATCH 449/516] Feature: certificate export command (v17.4.0) - new subcommand, `rcc configuration tlsexport`, to export TLS certificates from given set of secure and unsecure URLs - now tlsprobe reports fingerprint using sha256 from raw certificate, not just plain signature --- cmd/configureTLSexport.go | 46 ++++++++++ cmd/configureTLSprobe.go | 1 + common/version.go | 2 +- docs/changelog.md | 7 ++ fail/handling.go | 6 ++ operations/tlscheck.go | 172 +++++++++++++++++++++++++++++++++++++- 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 cmd/configureTLSexport.go diff --git a/cmd/configureTLSexport.go b/cmd/configureTLSexport.go new file mode 100644 index 00000000..52a8b333 --- /dev/null +++ b/cmd/configureTLSexport.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var ( + pemFile string +) + +var tlsExportCmd = &cobra.Command{ + Use: "tlsexport +", + Short: "Export TLS certificates from set of secure and unsecure URLs.", + Long: `Export TLS certificates from set of secure and unsecure URLs. + +CLI examples: + rcc configuration tlsexport --pemfile export.pem robot_urls.yaml + rcc configuration tlsexport --pemfile many.pem company_urls.yaml robot_urls.yaml more_urls.yaml + + +Configuration example in YAML format: +# trusted: +# - https://api.eu1.robocorp.com/ +# - https://pypi.org/ +# - https://files.pythonhosted.org/ +# untrusted: +# - https://self-signed.badssl.com/ +`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, configfiles []string) { + pretty.Guard(!pathlib.IsFile(common.CaBundleFile()), 1, "Cannot create certificate bundle, while profile provides %q!", common.CaBundleFile()) + err := operations.TLSExport(pemFile, configfiles) + pretty.Guard(err == nil, 2, "Probe failure: %v", err) + pretty.Ok() + }, +} + +func init() { + configureCmd.AddCommand(tlsExportCmd) + tlsExportCmd.Flags().StringVarP(&pemFile, "pemfile", "p", "", "Name of exported PEM file to write.") + tlsExportCmd.MarkFlagRequired("pemfile") +} diff --git a/cmd/configureTLSprobe.go b/cmd/configureTLSprobe.go index 3e4840cf..00b46e2f 100644 --- a/cmd/configureTLSprobe.go +++ b/cmd/configureTLSprobe.go @@ -36,6 +36,7 @@ Examples: rcc configuration tlsprobe outlook.office365.com:993 outlook.office365.com:995 rcc configuration tlsprobe api.us1.robocorp.com api.eu1.robocorp.com `, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { servers := fixHosts(args) err := operations.TLSProbe(servers) diff --git a/common/version.go b/common/version.go index fd58bc54..535f21e0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.3.1` + Version = `v17.4.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index d474bbb9..8195e0aa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.4.0 (date: 23.10.2023) WORK IN PROGRESS + +- new subcommand, `rcc configuration tlsexport`, to export TLS certificates + from given set of secure and unsecure URLs +- now tlsprobe reports fingerprint using sha256 from raw certificate, not + just plain signature + ## v17.3.1 (date: 18.10.2023) - minor fix: now used micromamba version number is stored in separate asset diff --git a/fail/handling.go b/fail/handling.go index f77da952..f57a554a 100644 --- a/fail/handling.go +++ b/fail/handling.go @@ -19,6 +19,12 @@ func Around(err *error) { *err = catch() } +func Fast(err error) { + if err != nil { + panic(failure("%v", err)) + } +} + func On(condition bool, form string, details ...interface{}) { if condition { panic(failure(form, details...)) diff --git a/operations/tlscheck.go b/operations/tlscheck.go index c54dc010..3efc61e4 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -2,22 +2,35 @@ package operations import ( "context" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" "crypto/tls" "crypto/x509" + "encoding/pem" "fmt" + "hash" "net" "net/http" + "os" "strings" "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/set" "github.com/robocorp/rcc/settings" + "gopkg.in/yaml.v2" ) type ( tlsConfigs []*tls.Config + + UrlConfig struct { + TrustedURLs []string `yaml:"trusted,omitempty"` + UntrustedURLs []string `yaml:"untrusted,omitempty"` + } ) var ( @@ -176,11 +189,34 @@ func configurationVariations(root *x509.CertPool) tlsConfigs { return configs } +func formatFingerprint(digest hash.Hash, certificate *x509.Certificate) string { + if certificate == nil { + return "N/A" + } + digest.Write(certificate.Raw) + return strings.Replace(fmt.Sprintf("% 02x", digest.Sum(nil)), " ", ":", -1) +} + +func md5Fingerprint(certificate *x509.Certificate) string { + return formatFingerprint(md5.New(), certificate) +} + +func sha1Fingerprint(certificate *x509.Certificate) string { + return formatFingerprint(sha1.New(), certificate) +} + +func sha256Fingerprint(certificate *x509.Certificate) string { + return formatFingerprint(sha256.New(), certificate) +} + func certificateFingerprint(certificate *x509.Certificate) string { if certificate == nil { return "[nil]" } - return fmt.Sprintf("[% 02X ...]", certificate.Signature[:10]) + digest := sha256.New() + digest.Write(certificate.Raw) + sum := digest.Sum(nil) + return fmt.Sprintf("[% 02X ...]", sum[:16]) } func dnsLookup(serverport string) string { @@ -278,3 +314,137 @@ func TLSProbe(serverports []string) (err error) { } return nil } + +func urlConfigFrom(content []byte) (*UrlConfig, error) { + result := new(UrlConfig) + err := yaml.Unmarshal(content, result) + if err != nil { + return nil, err + } + return result, nil +} + +func mergeConfigFiles(configfiles []string) (trusted []string, untrusted []string, err error) { + defer fail.Around(&err) + trusted, untrusted = []string{}, []string{} + for _, filename := range configfiles { + content, err := os.ReadFile(filename) + fail.On(err != nil, "Failed to read %q, reason: %v", filename, err) + config, err := urlConfigFrom(content) + fail.On(err != nil, "Failed to parse %q, reason: %v", filename, err) + trusted = append(trusted, config.TrustedURLs...) + untrusted = append(untrusted, config.UntrustedURLs...) + } + return trusted, untrusted, nil +} + +func certificateExport(certificate *x509.Certificate, trusted bool) (text string, err error) { + defer fail.Around(&err) + + label := "!UNTRUSTED" + if trusted { + label = "trusted" + } + + stream := &strings.Builder{} + if certificate != nil { + fmt.Fprintf(stream, "# Category: %q with flags:%010b (rcc %s)\n", label, certificate.KeyUsage, common.Version) + fmt.Fprintln(stream, "# Issuer:", certificate.Issuer) + fmt.Fprintln(stream, "# Subject:", certificate.Subject) + fmt.Fprintf(stream, "# Label: %q\n", certificate.Subject.CommonName) + fmt.Fprintf(stream, "# Serial: %d\n", certificate.SerialNumber) + fmt.Fprintf(stream, "# MD5 Fingerprint: %s\n", md5Fingerprint(certificate)) + fmt.Fprintf(stream, "# SHA1 Fingerprint: %s\n", sha1Fingerprint(certificate)) + fmt.Fprintf(stream, "# SHA256 Fingerprint: %s\n", sha256Fingerprint(certificate)) + block := &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw} + err := pem.Encode(stream, block) + fail.On(err != nil, "Could not PEM encode certificate, reason: %v", err) + } + return stream.String(), nil +} + +func pickVerifiedCertificates(roots *x509.CertPool, candidates []*x509.Certificate) ([]*x509.Certificate, []error) { + errors := make([]error, 0, len(candidates)) + verified := make([]*x509.Certificate, 0, len(candidates)) + toVerify := x509.VerifyOptions{Roots: roots} + for _, candidate := range candidates { + _, err := candidate.Verify(toVerify) + if err == nil { + verified = append(verified, candidate) + } else { + errors = append(errors, err) + } + } + return verified, errors +} + +func tlsExportUrls(roots *x509.CertPool, unique map[string]bool, urls []string, untrusted bool) (ok bool, err error) { + defer fail.Around(&err) + ok = true +search: + for _, url := range urls { + state, err := tlsCheckHeadOnly(url) + if err != nil { + ok = false + pretty.Warning("Failed to check URL %q for TLS certificates, reason: %v", url, err) + continue search + } + if state != nil { + total := len(state.PeerCertificates) + if total < 1 { + ok = false + pretty.Warning("Failed to check URL %q for TLS certificates, reason: too few certificates in chain", url) + continue search + } + exportable := state.PeerCertificates + if !untrusted { + verified, errors := pickVerifiedCertificates(roots, state.PeerCertificates) + if len(verified) == 0 { + pretty.Warning("Failed to verify any of TLS certificates for URL %q, reasons:", url) + for _, err := range errors { + pretty.Warning("- %v", err) + } + ok = false + continue search + } + exportable = verified + } + for _, export := range exportable { + text, err := certificateExport(export, !untrusted) + if err != nil { + pretty.Warning("Failed to export TLS certificates for URL %q, reason: %v", url, err) + ok = false + continue search + } + unique[text] = true + } + } else { + pretty.Warning("URL %q does not have TLS available!", url) + ok = false + } + } + return ok, nil +} + +func TLSExport(filename string, configfiles []string) (err error) { + defer fail.Around(&err) + + roots, err := x509.SystemCertPool() + fail.On(err != nil, "Cannot get system certificate pool, reason: %v", err) + + trustedURLs, untrustedURLs, err := mergeConfigFiles(configfiles) + fail.Fast(err) + + unique := make(map[string]bool) + trusted, err := tlsExportUrls(roots, unique, trustedURLs, false) + fail.Fast(err) + untrusted, err := tlsExportUrls(roots, unique, untrustedURLs, true) + fail.Fast(err) + if trusted && untrusted && len(unique) > 0 { + fullset := strings.Join(set.Keys(unique), "\n") + err := os.WriteFile(filename, []byte(fullset), 0o600) + fail.On(err != nil, "Failed to write certificate export file %q, reason: %v", filename, err) + return nil + } + return fmt.Errorf("Failed to export certificates. Reason unknown, maybe visible above. Flags are trusted:%v, untrusted:%v, count:%d.", trusted, untrusted, len(unique)) +} From 632f3dc8f3634539c924b15d02ea4f0734db5619 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 23 Oct 2023 15:24:54 +0300 Subject: [PATCH 450/516] Fix: certificate export functionality (v17.4.1) - verifying that tlsexport bundle can imported into certificate pool - using system certificate store as base (if available), and updating certificates there by default - fix on conda.yaml merging on pip options case - peeking `--debug` and `--trace` flags for preview of verbosity state --- cmd/configureTLSexport.go | 21 +++++++++++++++++++-- common/variables.go | 9 ++++++++- common/version.go | 2 +- conda/condayaml.go | 2 +- docs/changelog.md | 8 ++++++++ settings/settings.go | 13 +++++++++---- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/cmd/configureTLSexport.go b/cmd/configureTLSexport.go index 52a8b333..824f1c47 100644 --- a/cmd/configureTLSexport.go +++ b/cmd/configureTLSexport.go @@ -1,10 +1,14 @@ package cmd import ( + "crypto/x509" + "os" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -32,13 +36,26 @@ Configuration example in YAML format: `, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, configfiles []string) { - pretty.Guard(!pathlib.IsFile(common.CaBundleFile()), 1, "Cannot create certificate bundle, while profile provides %q!", common.CaBundleFile()) + pretty.Guard(!settings.Global.HasCaBundle(), 1, "Cannot create certificate bundle, while profile provides %q!", common.CaBundleFile()) err := operations.TLSExport(pemFile, configfiles) pretty.Guard(err == nil, 2, "Probe failure: %v", err) + err = certificatePool(pemFile) + pretty.Guard(err == nil, 3, "Could not import created CA bundle, reason: %v", err) pretty.Ok() }, } +func certificatePool(bundle string) (err error) { + defer fail.Around(&err) + + pool, err := x509.SystemCertPool() + fail.On(err != nil, "Could not get system certificate pool, reason: %v", err) + blob, err := os.ReadFile(bundle) + fail.On(err != nil, "Could not get read certificate bundle from %q, reason: %v", bundle, err) + fail.On(!pool.AppendCertsFromPEM(blob), "Could not add certs from %q to created pool!", bundle) + return nil +} + func init() { configureCmd.AddCommand(tlsExportCmd) tlsExportCmd.Flags().StringVarP(&pemFile, "pemfile", "p", "", "Name of exported PEM file to write.") diff --git a/common/variables.go b/common/variables.go index 4c856d7c..00a390e5 100644 --- a/common/variables.go +++ b/common/variables.go @@ -67,7 +67,14 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) // peek CLI options to pre-initialize "Warranty Voided" indicator - WarrantyVoidedFlag = set.Member(set.Set(os.Args), "--warranty-voided") + args := set.Set(os.Args) + WarrantyVoidedFlag = set.Member(args, "--warranty-voided") + if set.Member(args, "--debug") { + verbosity = Debugging + } + if set.Member(args, "--trace") { + verbosity = Tracing + } // Note: HololibCatalogLocation, HololibLibraryLocation and HololibUsageLocation // are force created from "htfs" direcotry.go init function diff --git a/common/version.go b/common/version.go index 535f21e0..7c861011 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.4.0` + Version = `v17.4.1` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 6e13fbf2..7311fd56 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -92,7 +92,7 @@ func (it *Dependency) IsExact() bool { } func (it *Dependency) SameAs(right *Dependency) bool { - return it.Name == right.Name + return !strings.HasPrefix(it.Name, "-") && it.Name == right.Name } func (it *Dependency) ExactlySame(right *Dependency) bool { diff --git a/docs/changelog.md b/docs/changelog.md index 8195e0aa..d0bc90de 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v17.4.1 (date: 23.10.2023) WORK IN PROGRESS + +- verifying that tlsexport bundle can imported into certificate pool +- using system certificate store as base (if available), and updating + certificates there by default +- fix on conda.yaml merging on pip options case +- peeking `--debug` and `--trace` flags for preview of verbosity state + ## v17.4.0 (date: 23.10.2023) WORK IN PROGRESS - new subcommand, `rcc configuration tlsexport`, to export TLS certificates diff --git a/settings/settings.go b/settings/settings.go index 329a2917..145bf735 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -243,20 +243,25 @@ func (it gateway) ConfiguredHttpTransport() *http.Transport { } func (it gateway) loadRootCAs() *x509.CertPool { + roots, err := x509.SystemCertPool() + if err != nil { + roots = x509.NewCertPool() + } + if !it.HasCaBundle() { - return nil + return roots } + + common.Debug("Using CA bundle file from %q.", common.CaBundleFile()) certificates, err := os.ReadFile(common.CaBundleFile()) if err != nil { common.Log("Warning! Problem reading %q, reason: %v", common.CaBundleFile(), err) - return nil + return roots } - roots := x509.NewCertPool() ok := roots.AppendCertsFromPEM(certificates) if !ok { common.Log("Warning! Problem appending sertificated from %q.", common.CaBundleFile()) - return nil } return roots } From a17f5a34b949ff3365f1c9f9ccde40048060810e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 25 Oct 2023 11:45:49 +0300 Subject: [PATCH 451/516] Various fixes detected while testing (v17.4.2) - minor fix: rcc point of view now has version number in it - new `--anything` flag to allow adding to command line something unique or note worthy about that specific line (had no effect what so ever) - technical: updated some go module dependencies --- cmd/root.go | 14 ++-- common/version.go | 2 +- docs/changelog.md | 7 ++ go.mod | 38 ++++++----- go.sum | 106 +++++++++++++++++-------------- operations/tlscheck.go | 1 + pretty/functions.go | 4 +- robot_tests/export_holozip.robot | 2 +- robot_tests/fullrun.robot | 6 +- robot_tests/templates.robot | 8 +-- 10 files changed, 108 insertions(+), 80 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index c7c40fdc..184c9c77 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,12 +18,13 @@ import ( ) var ( - profilefile string - profiling *os.File - versionFlag bool - silentFlag bool - debugFlag bool - traceFlag bool + anythingIgnore string + profilefile string + profiling *os.File + versionFlag bool + silentFlag bool + debugFlag bool + traceFlag bool ) func toplevelCommands(parent *cobra.Command) { @@ -107,6 +108,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&common.ControllerType, "controller", "user", "internal, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().StringVar(&common.SemanticTag, "tag", "transient", "semantic reason/context, why are you invoking rcc") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $ROBOCORP_HOME/rcc.yaml)") + rootCmd.PersistentFlags().StringVar(&anythingIgnore, "anything", "", "freeform string value that can be set without any effect, for example CLI versioning/reference") rootCmd.PersistentFlags().BoolVarP(&common.NoBuild, "no-build", "", false, "never allow building new environments, only use what exists already in hololib (also RCC_NO_BUILD=1)") rootCmd.PersistentFlags().BoolVarP(&silentFlag, "silent", "", false, "be less verbose on output (also RCC_VERBOSITY=silent)") diff --git a/common/version.go b/common/version.go index 7c861011..68352032 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.4.1` + Version = `v17.4.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index d0bc90de..6c44ccbf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.4.2 (date: 25.10.2023) + +- minor fix: rcc point of view now has version number in it +- new `--anything` flag to allow adding to command line something unique or + note worthy about that specific line (had no effect what so ever) +- technical: updated some go module dependencies + ## v17.4.1 (date: 23.10.2023) WORK IN PROGRESS - verifying that tlsexport bundle can imported into certificate pool diff --git a/go.mod b/go.mod index 9e53d692..c2fef479 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,37 @@ module github.com/robocorp/rcc go 1.20 require ( - github.com/dchest/siphash v1.2.2 + github.com/dchest/siphash v1.2.3 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-isatty v0.0.17 github.com/mitchellh/go-ps v1.0.0 - github.com/spf13/cobra v1.5.0 - github.com/spf13/viper v1.13.0 - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.17.0 + golang.org/x/sys v0.13.0 + golang.org/x/term v0.13.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/text v0.3.8 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 26aa0e61..1ffb6719 100644 --- a/go.sum +++ b/go.sum @@ -48,19 +48,19 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= -github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -100,7 +100,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -127,60 +127,64 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -191,15 +195,19 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -210,6 +218,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -264,6 +274,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -315,23 +326,26 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -475,8 +489,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/operations/tlscheck.go b/operations/tlscheck.go index 3efc61e4..9e0973d4 100644 --- a/operations/tlscheck.go +++ b/operations/tlscheck.go @@ -444,6 +444,7 @@ func TLSExport(filename string, configfiles []string) (err error) { fullset := strings.Join(set.Keys(unique), "\n") err := os.WriteFile(filename, []byte(fullset), 0o600) fail.On(err != nil, "Failed to write certificate export file %q, reason: %v", filename, err) + pretty.Highlight("Exported total of %d certificates into %q.", len(unique), filename) return nil } return fmt.Errorf("Failed to export certificates. Reason unknown, maybe visible above. Flags are trusted:%v, untrusted:%v, count:%d.", trusted, untrusted, len(unique)) diff --git a/pretty/functions.go b/pretty/functions.go index 7e877406..47b39351 100644 --- a/pretty/functions.go +++ b/pretty/functions.go @@ -9,7 +9,7 @@ import ( ) const ( - rccpov = `From rcc point of view, %q was` + rccpov = `From rcc %q point of view, %q was` maxSteps = 15 ) @@ -78,7 +78,7 @@ func Guard(truth bool, code int, format string, rest ...interface{}) { } func RccPointOfView(context string, err error) { - explain := fmt.Sprintf(rccpov, context) + explain := fmt.Sprintf(rccpov, common.Version, context) printer := Lowlight message := fmt.Sprintf("@@@ %s SUCCESS. @@@", explain) journal := fmt.Sprintf("%s SUCCESS.", explain) diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 194a7ad6..322e45dc 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -86,7 +86,7 @@ Goal: Can run as guest Prepare Robocorp Home tmp/guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Space created under author for guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 756658fd..6a1f9ac0 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -96,7 +96,7 @@ Goal: Run task in place in debug mode and with timeline. Must Have Version Must Have Origin Must Have Status - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Must Exist tmp/fluffy/output/environment_*_freeze.yaml Must Exist %{ROBOCORP_HOME}/wheels/ @@ -115,7 +115,7 @@ Goal: Run task in clean temporary directory. Wont Have Progress: 09/15 Must Have Progress: 14/15 Must Have Progress: 15/15 - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Merge two different conda.yaml files with conflict fails @@ -182,7 +182,7 @@ Goal: See variables from specific environment with robot.yaml but without task Step build/rcc holotree check --controller citests Goal: See variables from specific environment with warranty voided - Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml --warranty-voided + Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml --warranty-voided --anything I_know_what_Im_doing Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have RCC_EXE= diff --git a/robot_tests/templates.robot b/robot_tests/templates.robot index 092649c0..c1f0bdf3 100644 --- a/robot_tests/templates.robot +++ b/robot_tests/templates.robot @@ -17,7 +17,7 @@ Goal: Standard robot has correct hash. Goal: Running standard robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/standardi/robot.yaml Use STDERR - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Initialize new python robot. @@ -32,7 +32,7 @@ Goal: Python robot has correct hash. Goal: Running python robot is succesful. Step build/rcc task run --space templates --controller citests --robot tmp/pythoni/robot.yaml Use STDERR - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Initialize new extended robot. @@ -47,13 +47,13 @@ Goal: Extended robot has correct hash. Goal: Running extended robot is succesful. (Run All Tasks) Step build/rcc task run --space templates --task "Run All Tasks" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Running extended robot is succesful. (Run Example Task) Step build/rcc task run --space templates --task "Run Example Task" --controller citests --robot tmp/extendedi/robot.yaml Use STDERR - Must Have From rcc point of view, "actual main robot run" was SUCCESS. + Must Have point of view, "actual main robot run" was SUCCESS. Must Have OK. Goal: Correct holotree spaces were created. From f14ba098add92b91cec81e7e376e10685f926eef Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Oct 2023 14:59:22 +0200 Subject: [PATCH 452/516] Certificate variables and diagnostics (v17.5.0) - added `SSL_CERT_DIR` and `NODE_EXTRA_CA_CERTS` as environment variables when there is certificate bundle available - also added diagnostics of those environment variables (plus others) - minor documentation fixes - tutorial: example of easy robot run --- assets/man/tutorial.txt | 4 ++++ cmd/configureTLSexport.go | 3 +++ common/variables.go | 4 ++++ common/version.go | 2 +- conda/robocorp.go | 2 ++ docs/changelog.md | 8 ++++++++ operations/diagnostics.go | 14 ++++++++++++-- 7 files changed, 34 insertions(+), 3 deletions(-) diff --git a/assets/man/tutorial.txt b/assets/man/tutorial.txt index aee8c217..bff88f6b 100644 --- a/assets/man/tutorial.txt +++ b/assets/man/tutorial.txt @@ -4,6 +4,10 @@ Create you first Robot Framework or python robot and follow given instructions t rcc create +Easy way to run existing, unwrapped robot is just like this: + + rcc run --robot path/to/my/robot.yaml --task "My fine task" + Jumpstart your development with robot examples and templates from our community. Download and run any robot from Robocorp Portal https://robocorp.com/robots/ For example: diff --git a/cmd/configureTLSexport.go b/cmd/configureTLSexport.go index 824f1c47..ecd3c9e2 100644 --- a/cmd/configureTLSexport.go +++ b/cmd/configureTLSexport.go @@ -33,6 +33,9 @@ Configuration example in YAML format: # - https://files.pythonhosted.org/ # untrusted: # - https://self-signed.badssl.com/ + +Note: exported PEM file is sorted, so it can be easily compared to other export. + Use your fabourite diffing tool there. `, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, configfiles []string) { diff --git a/common/variables.go b/common/variables.go index 00a390e5..0cdace9b 100644 --- a/common/variables.go +++ b/common/variables.go @@ -298,6 +298,10 @@ func CaBundleFile() string { return ExpandPath(filepath.Join(RobocorpHome(), "ca-bundle.pem")) } +func CaBundleDir() string { + return ExpandPath(RobocorpHome()) +} + func DefineVerbosity(silent, debug, trace bool) { override := os.Getenv(RCC_VERBOSITY) switch { diff --git a/common/version.go b/common/version.go index 68352032..44d7d51e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.4.2` + Version = `v17.5.0` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 5e19e994..1e4f3007 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -173,6 +173,8 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st environment = appendIfValue(environment, "REQUESTS_CA_BUNDLE", common.CaBundleFile()) environment = appendIfValue(environment, "CURL_CA_BUNDLE", common.CaBundleFile()) environment = appendIfValue(environment, "SSL_CERT_FILE", common.CaBundleFile()) + environment = appendIfValue(environment, "SSL_CERT_DIR", common.CaBundleDir()) + environment = appendIfValue(environment, "NODE_EXTRA_CA_CERTS", common.CaBundleFile()) } return environment } diff --git a/docs/changelog.md b/docs/changelog.md index 6c44ccbf..0c70779a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v17.5.0 (date: 30.10.2023) + +- added `SSL_CERT_DIR` and `NODE_EXTRA_CA_CERTS` as environment variables + when there is certificate bundle available +- also added diagnostics of those environment variables (plus others) +- minor documentation fixes +- tutorial: example of easy robot run + ## v17.4.2 (date: 25.10.2023) - minor fix: rcc point of view now has version number in it diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 3009a4a7..d3bca940 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -138,10 +138,20 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { if check != nil { result.Checks = append(result.Checks, check) } - result.Checks = append(result.Checks, anyPathCheck("PYTHONPATH")) - result.Checks = append(result.Checks, anyPathCheck("PLAYWRIGHT_BROWSERS_PATH")) + + result.Checks = append(result.Checks, anyPathCheck("CURL_CA_BUNDLE")) + result.Checks = append(result.Checks, anyPathCheck("NODE_EXTRA_CA_CERTS")) result.Checks = append(result.Checks, anyPathCheck("NODE_OPTIONS")) result.Checks = append(result.Checks, anyPathCheck("NODE_PATH")) + result.Checks = append(result.Checks, anyPathCheck("NODE_TLS_REJECT_UNAUTHORIZED")) + result.Checks = append(result.Checks, anyPathCheck("PIP_CONFIG_FILE")) + result.Checks = append(result.Checks, anyPathCheck("PLAYWRIGHT_BROWSERS_PATH")) + result.Checks = append(result.Checks, anyPathCheck("PYTHONPATH")) + result.Checks = append(result.Checks, anyPathCheck("REQUESTS_CA_BUNDLE")) + result.Checks = append(result.Checks, anyPathCheck("SSL_CERT_DIR")) + result.Checks = append(result.Checks, anyPathCheck("SSL_CERT_FILE")) + result.Checks = append(result.Checks, anyPathCheck("WDM_SSL_VERIFY")) + if !common.OverrideSystemRequirements() { result.Checks = append(result.Checks, longPathSupportCheck()) } From c96f58055153e5997e7ea23fdbba11d9591fb744 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 2 Nov 2023 09:34:35 +0200 Subject: [PATCH 453/516] Simple relocation detection (v17.6.0) - replaced trollhash with simple relocation detection algorithm and remove trollhash from codebase --- common/version.go | 2 +- docs/changelog.md | 5 ++ htfs/functions.go | 3 +- htfs/relocator.go | 81 ++++++++++++++++++ htfs/relocator_test.go | 48 +++++++++++ trollhash/algorithm.go | 175 --------------------------------------- trollhash/rrhash_test.go | 60 -------------- 7 files changed, 136 insertions(+), 238 deletions(-) create mode 100644 htfs/relocator.go create mode 100644 htfs/relocator_test.go delete mode 100644 trollhash/algorithm.go delete mode 100644 trollhash/rrhash_test.go diff --git a/common/version.go b/common/version.go index 44d7d51e..7f3d9847 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.5.0` + Version = `v17.6.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0c70779a..9ae3dce1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.6.0 (date: 2.11.2023) WORK IN PROGRESS + +- replaced trollhash with simple relocation detection algorithm and remove + trollhash from codebase + ## v17.5.0 (date: 30.10.2023) - added `SSL_CERT_DIR` and `NODE_EXTRA_CA_CERTS` as environment variables diff --git a/htfs/functions.go b/htfs/functions.go index 7a15e212..a1a9334e 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -14,7 +14,6 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/trollhash" ) func JustFileExistCheck(library MutableLibrary, path, name, digest string) anywork.Work { @@ -131,7 +130,7 @@ func Locator(seek string) Filetask { } defer source.Close() digest := sha256.New() - locator := trollhash.LocateWriter(digest, seek) + locator := RelocateWriter(digest, seek) _, err = io.Copy(locator, source) if err != nil { panic(fmt.Sprintf("Copy[Locator] %q, reason: %v", fullpath, err)) diff --git a/htfs/relocator.go b/htfs/relocator.go new file mode 100644 index 00000000..d58ac448 --- /dev/null +++ b/htfs/relocator.go @@ -0,0 +1,81 @@ +package htfs + +import ( + "bytes" + "io" +) + +type ( + WriteLocator interface { + io.Writer + Locations() []int64 + } + + simple struct { + windowsize int + window []byte + trigger byte + needle []byte + history int64 + delegate io.Writer + found []int64 + } +) + +func RelocateWriter(delegate io.Writer, needle string) WriteLocator { + blob := []byte(needle) + windowsize := len(blob) + result := &simple{ + windowsize: windowsize, + window: []byte{}, + trigger: blob[windowsize-1], + needle: blob, + history: 0, + delegate: delegate, + found: make([]int64, 0, 20), + } + return result +} + +func (it *simple) trimWindow() { + total := len(it.window) + if total > it.windowsize { + it.window = it.window[total-it.windowsize:] + } +} + +func (it *simple) Write(payload []byte) (int, error) { + pending := len(it.window) + it.window = append(it.window, payload...) + defer it.trimWindow() + + shift, view, trigger, limit := 0, it.window, it.trigger, it.windowsize +search: + for limit < len(view) { + found := bytes.IndexByte(view, trigger) + if found < 0 { + break search + } + head := view[:found+1] + view = view[found+1:] + end := shift + found + 1 - pending + start := end - limit + headsize := len(head) + if limit <= headsize { + candidate := head[headsize-limit:] + relation := bytes.Compare(it.needle, candidate) + if relation == 0 { + it.found = append(it.found, it.history+int64(start)) + } + } + shift += len(head) + } + + // seek here when found, append to it.found + it.history += int64(len(payload)) + return it.delegate.Write(payload) +} + +func (it *simple) Locations() []int64 { + return it.found +} diff --git a/htfs/relocator_test.go b/htfs/relocator_test.go new file mode 100644 index 00000000..25ee5a68 --- /dev/null +++ b/htfs/relocator_test.go @@ -0,0 +1,48 @@ +package htfs_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/htfs" +) + +func TestBasics(t *testing.T) { + must, wont := hamlet.Specifications(t) + + sut := "simple text" + must.Equal(11, len(sut)) + wont.Panic(func() { + fmt.Sprintf("%q", sut[:11]) + }) + wont.Panic(func() { + fmt.Sprintf("%q", sut[11:]) + }) + must.Panic(func() { + fmt.Sprintf("%q", sut[:12]) + }) + must.Panic(func() { + fmt.Sprintf("%q", sut[12:]) + }) + must.Equal(sut, sut[:len(sut)]) + must.Equal("", sut[len(sut):]) +} + +func TestUsingNonhashingWorks(t *testing.T) { + must, wont := hamlet.Specifications(t) + + sink := bytes.NewBuffer(nil) + sut := htfs.RelocateWriter(sink, "loha") + + wont.Nil(sut) + size, err := sut.Write([]byte("O aloha! Aloha, Holoham!")) + must.Nil(err) + must.Equal(24, size) + must.Equal([]int64{3, 10, 18}, sut.Locations()) + size, err = sut.Write([]byte("O aloha! Aloha, Holoham!")) + must.Nil(err) + must.Equal(24, size) + must.Equal([]int64{3, 10, 18, 27, 34, 42}, sut.Locations()) +} diff --git a/trollhash/algorithm.go b/trollhash/algorithm.go deleted file mode 100644 index da4579d0..00000000 --- a/trollhash/algorithm.go +++ /dev/null @@ -1,175 +0,0 @@ -package trollhash - -import ( - "io" -) - -type Trollhash func(byte) uint64 - -func New(size int) Trollhash { - lshift := (3 * size) % 64 - rshift := 64 - lshift - history := make([]uint64, size) - slot := 0 - troll := uint64(0) - return func(key byte) uint64 { - remove := history[slot] - tool := seedlings[key] - history[slot] = tool - troll = (troll << 3) | (troll >> 61) ^ tool - troll ^= (remove << lshift) | (remove >> rshift) - slot += 1 - if slot == size { - slot = 0 - } - return troll - } -} - -func Hash(needle []byte) (result uint64) { - hasher := New(len(needle)) - for _, add := range needle { - result = hasher(add) - } - return result -} - -type WriteLocator interface { - io.Writer - Locations() []int64 -} - -type writer struct { - delegate io.Writer - seek Seeker - found []int64 -} - -func (it *writer) Write(payload []byte) (int, error) { - for _, value := range payload { - ok, head := it.seek(value) - if ok { - it.found = append(it.found, head) - } - } - return it.delegate.Write(payload) -} - -func (it *writer) Locations() []int64 { - return it.found -} - -func LocateWriter(delegate io.Writer, needle string) WriteLocator { - result := &writer{ - delegate: delegate, - seek: Find(needle), - found: make([]int64, 0, 20), - } - return result -} - -type Seeker func(byte) (bool, int64) - -func makeSeeker(verify []byte) Seeker { - goal := Hash(verify) - limit := int64(len(verify)) - cursor, window := int64(-1), make([]byte, limit) - hasher := New(int(limit)) - slot, size := 0, int(limit) - return func(add byte) (bool, int64) { - cursor += 1 - window[slot] = add - slot += 1 - if slot == size { - slot = 0 - } - if hasher(add) != goal { - return false, -1 - } - for at, value := range verify { - if value != window[(cursor+1+int64(at))%limit] { - return false, -1 - } - } - return true, cursor - limit + 1 - } -} - -func Find(needle string) Seeker { - return makeSeeker([]byte(needle)) -} - -func Seedlings() map[uint64]int { - result := make(map[uint64]int) - for at, value := range seedlings { - result[value] = at - } - return result -} - -var seedlings = [256]uint64{ - 0x602b8129934c868b, 0x6ab8983d8c8ba01c, 0x54d04ec8dee35ff5, 0xfdc3567e6f901fd5, - 0x2562b4b5b0cc366c, 0xab60180bab46c43f, 0x5a54553f73bd3e1b, 0xaf194e0fe6c4bf87, - 0x25c98c0d6b63a370, 0x8c81fae8b37fff21, 0x53ead3efb8b5b25d, 0xcce5c646782cd9cf, - 0xfdf778b8ae365720, 0x4d977df169207cca, 0x156a5b75bc7798b5, 0x46c7f1335c2ed747, - 0x8310228569b88651, 0x35809eeb9ca50863, 0x441ee622f898ad0a, 0xc5bb8b2cf3932d6b, - 0xf7c606cabd736bc6, 0x28b4e86876eeedb9, 0xb49d9c3599dd647e, 0xda85b43fa53edcf2, - 0x392ee67addd0d02f, 0x73a31ef6d1b95fba, 0x5e169bbb3d28951b, 0x6969557639c6a9e8, - 0xb03c20f52a6f3fd4, 0x27ffff7cf607addd, 0x335c0951697ce069, 0xf40a8371a53c31dc, - 0x13a765069f736cc6, 0x942216377a8d67e3, 0xed0c5da61b167d3a, 0xaa9d5e228b5567a8, - 0x4cd448389d866d4f, 0x93d6a2e30f18e84f, 0xf8be8616379c8db1, 0x95c7c233cc922e36, - 0x8021cb619a850787, 0x7f6d5edea66f25a9, 0x0e6c9d1e6c9646f9, 0x02b9a1ab0b82bb32, - 0x8a4344782e76446f, 0xa93d1c7ff54f35cd, 0x303e4f8594da3e66, 0x8d034c3bcc43340e, - 0x70977337f51155a0, 0x750701470ef3de59, 0x3c57e01aeb3a9e59, 0xd583288188c6289a, - 0x656d0c50ea6bb54d, 0x76bbc1e73deb85ef, 0x40db7a12e5c065a3, 0xce585e8b46166d1d, - 0x284ee3e3fe8aa20b, 0x3f315e2464e9d196, 0x6f5b08ad33872bd9, 0x00b1405c606adb9e, - 0xbf20e1957769961a, 0x297ba8e2d1903af3, 0x6f903a275e60451e, 0x87a83971fb761024, - 0x9348cb5ff1383281, 0x2f203fec99682f8d, 0x1443e52adddcf08e, 0xef09ff3b737a55fc, - 0xe92cd8b27e851a79, 0x6e1e59a28c13f09a, 0x24a7c49a9bade515, 0xff3045ffc77d2b24, - 0xf16f51fa093ccdc6, 0xdf04734eec39671e, 0x76b6f9adb5c2d094, 0x3904a429219a48ef, - 0x9d0244a46fd87f84, 0x53177c2f2b3465d9, 0xf27b02832137d20b, 0x72a45d5c27ef2bde, - 0x8c8307bfc4117674, 0x69ca61ca73e8113d, 0x244eb5285055a241, 0xa57fa8ef3f85efc8, - 0x607d3dab80e04aa0, 0x7e47163b689e8c81, 0xcddc93876c73a1a8, 0x94d4635b1fcaa2e3, - 0xf1283c1bbc591b52, 0x54098cf2b11c8d68, 0x5b181f79ddc50186, 0x77c57d4e824a4636, - 0x39f888233f81fc4e, 0x5eb7eb313d175801, 0xad4b57e527f01949, 0xb91b4230e16f2edc, - 0x92a0676324bf9721, 0x384bf9513d1fb244, 0x40cf2ef187ef03cf, 0x6f12ddbbd8383773, - 0xa3110f4ead8a066a, 0x1b6ab1431567bb2d, 0x73a397be57c72c8a, 0x4c9c445d1db0c18c, - 0xc6cb2c15ed2b1faa, 0x83a988ce9c10d893, 0xaf4fe6805be23828, 0x776a74a4dec3cd7e, - 0xd23d9949d80c389b, 0xdbd6399f812a0c56, 0xc5bb1c90121ca7ce, 0x332cda9b9ff5afb9, - 0x5a6239acaafceabd, 0x0258f2053a726194, 0x142f72cfc81201cf, 0x85af1c1c2b3d0425, - 0x620568ce9f81d404, 0x28cc4d813b157eb7, 0x36637f0bc3f48ed8, 0x563c25e210789612, - 0x0ef4909420333fae, 0x8fd529de3bbc7a70, 0x8989297984fcc92d, 0x482f38b5985919bf, - 0x8b3604417bc20181, 0x8aa0bd889a5496cb, 0x38d69325a3b94aa1, 0x4b3d5b0c247c8316, - 0xc3bb81aa4a2b8f1e, 0x48baa02799ec924b, 0xd287c474ad6a3d0d, 0xf93312bf407b38a3, - 0x1a62a35cacbabca2, 0x266fdbb2033d743e, 0xac80c27e4a760ce1, 0x8995461d77a933c0, - 0x3ad00ba59d07b4f3, 0x969a9e95f9617ae3, 0x1079fe07d879543d, 0x31880d8a6420cb8c, - 0x84eba52cbad9d38a, 0x6477aa7aebf5d8b2, 0x31c5bcc065ae3124, 0x5e1b2121b423868e, - 0x3f208e19c74b0994, 0xc3ed021162e50000, 0x49a71a2f28d1732b, 0xcbe5d2df36846b2b, - 0xf3b59192f546f437, 0x0d826b0d72b2fd15, 0x6eeaa0c0a2ffb7f2, 0x6113e0030b7d5908, - 0x3659c2043e8aee58, 0xf1060b073baf9339, 0xc072daff6bc681f8, 0x884345ae6ef6c538, - 0x202184833fd0bbbf, 0xd266a3bb47fc22f1, 0x8914c38ffeaa392e, 0x73ce11a141170ea1, - 0xc0df84b348e03fbf, 0x6d745be300145ac8, 0x063b321ab6d0fad8, 0xf87bc2d24666b17e, - 0x320cd31b8df1ef33, 0xc1ec122e2fd5bb39, 0x74f5923d5d2b5eba, 0xe85798e5f5cf02c5, - 0x83360efbfe2ffae2, 0x79a0dba643e4b98a, 0x87512888e7e12293, 0xb1fd433d8a37043a, - 0xbad8d58a167d3ca5, 0x99855b2b011c29cc, 0x4fcfde91f1ef652a, 0xec462ce2c5b2a730, - 0xb2b03ac73d5de194, 0x9565e9662275f9aa, 0x1f5a09117923a94d, 0x201b3c78cc80bb5f, - 0x105da69edd31574f, 0x9444318eb5e5af8d, 0xb4bc0f4f23295ef7, 0x86988e0c3546863b, - 0x1827e710952a0df2, 0x84af4a83f577e63e, 0x477bee7db82f88d1, 0xc7808fa972ea660e, - 0x0904fe12acff3f63, 0xb5baf8db3fb767f1, 0xe675059b5b603db1, 0xcf34d51dbddd9733, - 0xf7dc0a20ebc5f184, 0xb49588039d34ee77, 0x3015fa3d8f3145f9, 0x26afeef62e3a9a29, - 0x174257d586d9a3f2, 0x9aa8876a6a5eafd5, 0xde5150df0c3eef53, 0x3ba44df0da99cb4a, - 0xcf8668774135287e, 0xe8abe6669fb8aa4d, 0xe8a72dcf45e862fa, 0xcf11e3d463f05295, - 0xdd8862d9877b910b, 0x1d4cc1684ee326fc, 0x98e7907b726a7b53, 0xad845110e13a3c48, - 0x37de32a13c2d959a, 0xecfbec7d1a2c4339, 0xb2334e53257814a9, 0xe287b65ac7e079d8, - 0x2f44ae0cecfd15ff, 0xf71e440c162d323f, 0x4ebc72f70a4438c4, 0x9972e1d375126584, - 0xdf43537388eecfb8, 0x93f1597d9f115c0b, 0x3e7c5ab2ce23d493, 0x01bcd6f5d1b559ae, - 0xae3a89d1c2ab3c97, 0x224c3ad9e70defa3, 0x8cc7be5774b6a234, 0x07c5a14d71baff0e, - 0xef36700d8e824543, 0x1a730b83ccac66bd, 0x2179a3d23e72bec5, 0xbdd5c3445c00db14, - 0xc9f22df506ca595d, 0x70c58e3b4b014d74, 0x938755c1c7e11634, 0x2ce1a74793a461bd, - 0xbf1f4a2f14f594ef, 0x95ec2bce4bf8481a, 0xe9d8809a5065a5b4, 0x6cec014177d56fbf, - 0xb68c5f723764db37, 0xa2f5f21314b3935f, 0x3d3289499254d863, 0x85a5b5667439987d, - 0x31710194f888f426, 0x61e713b19996c044, 0x922938c7acbf5a69, 0xb7d3bcf2f449d3b9, - 0x24401236b825b103, 0xb6067fe7627e6b5a, 0x74732e6e3fc80d22, 0xa172f36ec342041e, - 0xd1465ccc18cf6df3, 0xf4fb5eb5ccf459a2, 0x6b22cce719a2c185, 0xcf41f79318890218, - 0x1d65d8c00d13b16c, 0xf1d1fd3f8c5c3c81, 0xabf782ddcdc96476, 0x62aed2ded00413ca, -} diff --git a/trollhash/rrhash_test.go b/trollhash/rrhash_test.go deleted file mode 100644 index bdce2cd8..00000000 --- a/trollhash/rrhash_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package trollhash_test - -import ( - "math/rand" - "testing" - "time" - - "github.com/robocorp/rcc/hamlet" - "github.com/robocorp/rcc/trollhash" -) - -func TestUsingRollingHashWorks(t *testing.T) { - must, wont := hamlet.Specifications(t) - - must.Equal(12345, 12345^0) - must.Equal(9876543210, 9876543210^0) - - must.Equal(256, len(trollhash.Seedlings())) - - must.Equal(uint64(0x2f203fec99682f8d), trollhash.Hash([]byte("A"))) - must.Equal(uint64(0x1443e52adddcf08e), trollhash.Hash([]byte("B"))) - must.Equal(uint64(0xef09ff3b737a55fc), trollhash.Hash([]byte("C"))) - must.Equal(uint64(0x6d421a4e169d8ce7), trollhash.Hash([]byte("AB"))) - must.Equal(uint64(0x85192d4bc79632c7), trollhash.Hash([]byte("ABC"))) - - rolling := trollhash.Find("loha") - wont.Nil(rolling) - result := make([]int64, 0) - for _, step := range []byte("O aloha! Aloha, Holoham!") { - ok, at := rolling(step) - if ok { - result = append(result, at) - } - } - must.Equal([]int64{3, 10, 18}, result) - must.Equal(uint64(0xbe16aca9b15d96fa), trollhash.Hash([]byte("loha"))) - limit := 256 - for key := 0; key < 256; key++ { - result := make(map[uint64]bool) - flow := make([]byte, 0, limit) - for size := 0; size < limit; size++ { - flow = append(flow, byte(key)) - result[trollhash.Hash(flow)] = true - } - must.Equal(256, len(flow)) - must.True(63 < len(result)) - must.True(len(result) < 129) - } - limit = 10240 - uniques := make(map[uint64]bool) - flow := make([]byte, 0, limit) - source := rand.NewSource(time.Now().UnixNano()) - rnd := rand.New(source) - for count := 0; count < limit; count++ { - flow = append(flow, byte(rnd.Uint32())) - uniques[trollhash.Hash(flow)] = true - } - must.Equal(10240, len(flow)) - must.Equal(10240, len(uniques)) -} From 4bf7aa4d8b304377f6a14c2b7bae1d763f4544d3 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 6 Nov 2023 15:52:03 +0200 Subject: [PATCH 454/516] SSL fix and performance improvements (v17.6.1) - removed experimental `SSL_CERT_DIR` as environment variable, since it might actually be confusing to have there (but diagnostics will remain) - removed duplicate work on checking catalog integrity which was called during holotree restore --- common/version.go | 2 +- conda/robocorp.go | 1 - docs/changelog.md | 7 +++++++ htfs/functions.go | 9 ++++++--- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 7f3d9847..76d6c5ae 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.6.0` + Version = `v17.6.1` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 1e4f3007..2e67d3ab 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -173,7 +173,6 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st environment = appendIfValue(environment, "REQUESTS_CA_BUNDLE", common.CaBundleFile()) environment = appendIfValue(environment, "CURL_CA_BUNDLE", common.CaBundleFile()) environment = appendIfValue(environment, "SSL_CERT_FILE", common.CaBundleFile()) - environment = appendIfValue(environment, "SSL_CERT_DIR", common.CaBundleDir()) environment = appendIfValue(environment, "NODE_EXTRA_CA_CERTS", common.CaBundleFile()) } return environment diff --git a/docs/changelog.md b/docs/changelog.md index 9ae3dce1..e33442c9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.6.1 (date: 6.11.2023) WORK IN PROGRESS + +- removed experimental `SSL_CERT_DIR` as environment variable, since it might + actually be confusing to have there (but diagnostics will remain) +- removed duplicate work on checking catalog integrity which was called + during holotree restore + ## v17.6.0 (date: 2.11.2023) WORK IN PROGRESS - replaced trollhash with simple relocation detection algorithm and remove diff --git a/htfs/functions.go b/htfs/functions.go index a1a9334e..9ffb92cd 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -16,9 +16,8 @@ import ( "github.com/robocorp/rcc/pathlib" ) -func JustFileExistCheck(library MutableLibrary, path, name, digest string) anywork.Work { +func justFileExistCheck(location string, path, name, digest string) anywork.Work { return func() { - location := library.ExactLocation(digest) if !pathlib.IsFile(location) { fullpath := filepath.Join(path, name) panic(fmt.Errorf("Content for %q [%s] is missing; hololib is broken, requires check!", fullpath, digest)) @@ -28,9 +27,13 @@ func JustFileExistCheck(library MutableLibrary, path, name, digest string) anywo func CatalogCheck(library MutableLibrary, fs *Root) Treetop { var tool Treetop + scheduled := make(map[string]bool) tool = func(path string, it *Dir) error { for name, file := range it.Files { - anywork.Backlog(JustFileExistCheck(library, path, name, file.Digest)) + if !scheduled[file.Digest] { + anywork.Backlog(justFileExistCheck(library.ExactLocation(file.Digest), path, name, file.Digest)) + scheduled[file.Digest] = true + } } for name, subdir := range it.Dirs { err := tool(filepath.Join(path, name), subdir) From f2dbdb7fec6409ef9f5f3edc7725c15d0c687a87 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Nov 2023 09:05:35 +0200 Subject: [PATCH 455/516] Holotree space use tracking (v17.7.0) - added simple tracking of used holotree spaces - added "Last used" and "Use count" to holotree space listings --- cmd/holotreeList.go | 28 +++++++++++++++++++++++----- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 3 +++ pathlib/functions.go | 13 +++++++++++++ 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/cmd/holotreeList.go b/cmd/holotreeList.go index 34f347c2..94aa1ef2 100644 --- a/cmd/holotreeList.go +++ b/cmd/holotreeList.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "text/tabwriter" + "time" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" @@ -13,25 +14,38 @@ import ( "github.com/spf13/cobra" ) +func whatUsage(space string) (string, string, int) { + usefile := fmt.Sprintf("%s.use", space) + stat, err := os.Stat(usefile) + if err != nil { + return "N/A", "N/A", 0 + } + times := fmt.Sprintf("%d times", stat.Size()) + delta := time.Now().Sub(stat.ModTime()).Hours() / 24.0 + when := fmt.Sprintf("%1.0f days ago", delta) + return when, times, int(delta) +} + func humaneHolotreeSpaceListing() { tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) - tabbed.Write([]byte("Identity\tController\tSpace\tBlueprint\tFull path\n")) - tabbed.Write([]byte("--------\t----------\t-----\t--------\t---------\n")) + tabbed.Write([]byte("Identity\tController\tSpace\tBlueprint\tFull path\tLast used\tUse count\n")) + tabbed.Write([]byte("--------\t----------\t-----\t---------\t---------\t---------\t---------\n")) _, roots := htfs.LoadCatalogs() for _, space := range roots.Spaces() { - data := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", space.Identity, space.Controller, space.Space, space.Blueprint, space.Path) + when, times, _ := whatUsage(space.Path) + data := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%s\n", space.Identity, space.Controller, space.Space, space.Blueprint, space.Path, when, times) tabbed.Write([]byte(data)) } tabbed.Flush() } func jsonicHolotreeSpaceListing() { - details := make(map[string]map[string]string) + details := make(map[string]map[string]any) _, roots := htfs.LoadCatalogs() for _, space := range roots.Spaces() { hold, ok := details[space.Identity] if !ok { - hold = make(map[string]string) + hold = make(map[string]any) details[space.Identity] = hold hold["id"] = space.Identity hold["controller"] = space.Controller @@ -41,6 +55,10 @@ func jsonicHolotreeSpaceListing() { hold["meta"] = space.Path + ".meta" hold["spec"] = filepath.Join(space.Path, "identity.yaml") hold["plan"] = filepath.Join(space.Path, "rcc_plan.log") + when, times, idle := whatUsage(space.Path) + hold["last-used"] = when + hold["idle-days"] = idle + hold["use-count"] = times } } body, err := json.MarshalIndent(details, "", " ") diff --git a/common/version.go b/common/version.go index 76d6c5ae..9ee077ab 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.6.1` + Version = `v17.7.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index e33442c9..97d8b5f5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.7.0 (date: 8.11.2023) + +- added simple tracking of used holotree spaces +- added "Last used" and "Use count" to holotree space listings + ## v17.6.1 (date: 6.11.2023) WORK IN PROGRESS - removed experimental `SSL_CERT_DIR` as environment variable, since it might diff --git a/htfs/commands.go b/htfs/commands.go index cc859b83..29481b23 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -1,6 +1,7 @@ package htfs import ( + "fmt" "os" "os/user" "path/filepath" @@ -53,6 +54,8 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal pretty.Regression(15, "Holotree restoration failure, see above [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) } else { pretty.Progress(15, "Fresh holotree done [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) + usefile := fmt.Sprintf("%s.use", path) + pathlib.AppendFile(usefile, []byte{'.'}) } if haszip { pretty.Note("There is hololib.zip present at: %q", holozip) diff --git a/pathlib/functions.go b/pathlib/functions.go index ef02ea99..030622ad 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -321,3 +321,16 @@ func RemoveEmptyDirectores(starting string) (err error) { } }) } + +func AppendFile(filename string, blob []byte) (err error) { + defer fail.Around(&err) + if common.WarrantyVoided() { + return nil + } + handle, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) + fail.On(err != nil, "Failed to open file %v -> %v", filename, err) + defer handle.Close() + _, err = handle.Write(blob) + fail.On(err != nil, "Failed to write file %v -> %v", filename, err) + return handle.Sync() +} From 6cce92487c36c1c07f1de985b7523c6af2226556 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Nov 2023 10:03:30 +0200 Subject: [PATCH 456/516] Holotree space use tracking fix (v17.7.1) - bugfix: changed used holotree space tracking so, that it is visible to everybody on file level --- common/version.go | 2 +- docs/changelog.md | 7 ++++++- pathlib/functions.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 9ee077ab..37b135ed 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.7.0` + Version = `v17.7.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 97d8b5f5..67353379 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # rcc change log -## v17.7.0 (date: 8.11.2023) +## v17.7.1 (date: 8.11.2023) + +- bugfix: changed used holotree space tracking so, that it is visible to + everybody on file level + +## v17.7.0 (date: 8.11.2023) INCOMPLETE - added simple tracking of used holotree spaces - added "Last used" and "Use count" to holotree space listings diff --git a/pathlib/functions.go b/pathlib/functions.go index 030622ad..651d961c 100644 --- a/pathlib/functions.go +++ b/pathlib/functions.go @@ -327,7 +327,7 @@ func AppendFile(filename string, blob []byte) (err error) { if common.WarrantyVoided() { return nil } - handle, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) + handle, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) fail.On(err != nil, "Failed to open file %v -> %v", filename, err) defer handle.Close() _, err = handle.Write(blob) From e9a188eb7f430e24c09f613aa03683de4804bffc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Nov 2023 12:52:27 +0200 Subject: [PATCH 457/516] Documentation updates (v17.7.2) - documentation updates on maintenance, and vocabulary, etc. --- common/version.go | 2 +- docs/changelog.md | 4 ++++ docs/features.md | 5 +++-- docs/maintenance.md | 5 ++++- docs/recipes.md | 4 ++++ docs/troubleshooting.md | 3 +++ docs/vocabulary.md | 5 ++++- 7 files changed, 23 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 37b135ed..37945e35 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.7.1` + Version = `v17.7.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 67353379..09072f0b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.7.2 (date: 8.11.2023) + +- documentation updates on maintenance, and vocabulary, etc. + ## v17.7.1 (date: 8.11.2023) - bugfix: changed used holotree space tracking so, that it is visible to diff --git a/docs/features.md b/docs/features.md index dbb162f1..3dee01d4 100644 --- a/docs/features.md +++ b/docs/features.md @@ -2,13 +2,14 @@ * supported operating systems are Windows, MacOS, and Linux * supported sources for environment building are both conda and pypi -* provide repeatable, isolated, and clean environments for robots to run +* provide repeatable, isolated, and clean environments for automations and + robots to run on * automatic environment creation based on declarative conda environment.yaml files * easily run software robots (automations) based on declarative robot.yaml files * test robots in isolated environments before uploading them to Control Room * provide commands for Robocorp runtime and developer tools (Worker, Assistant, - VS Code, Automation Studio ...) + VS Code, ...) * provides commands to communicate with Robocorp Control Room from command line * enable caching dormant environments in efficiently and activating them locally when required without need to reinstall anything diff --git a/docs/maintenance.md b/docs/maintenance.md index 91ae565d..9c10a81d 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -8,6 +8,7 @@ holotree/hololib setup. There are number of reasons for doing maintenance, some of which are: - running into problem with using holotree, and wanting to start over +- something breaks in holotree/lib and there is need to fix it - running out of disk space, and wanting to reduced used foot print - remove old/unused spaces from holotree - remove old/unused catalogs from hololib @@ -46,7 +47,9 @@ Catalogs can be listed using `rcc holotree catalogs` command, and if you add `--identity` you can see what was their environment specification. Then command `rcc holotree list` is used to list those concrete spaces that -are consuming your disk space. +are consuming your disk space. There you can also see how many times space +has been used, and when was last time it was used. (And using in this context +means, that rcc did create or refresh that specific space.) Once you know what is there, and there are needs to remove catalogs, then see `rcc holotree remove -h` for more about information on that. One good diff --git a/docs/recipes.md b/docs/recipes.md index dcfc949e..d57c24ce 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -477,6 +477,8 @@ rcc configure speedtest - generic flag `--debug` shows debug messages during execution - generic flag `--trace` shows more verbose debugging messages during execution - flag `--timeline` can be used to see execution timeline and where time was spent +- with option `--pprof ` enable profiling if performance is problem, + and want to help improve it (by submitting that profile file to developers) ## Advanced network diagnostics @@ -645,6 +647,8 @@ option is missing, the `devTasks:` will be skipped/missing, and the normal ### What is `condaConfigFile:`? +> Use of this is deprecated, please use `environmentConfigs:` instead. + This is actual name used as `conda.yaml` environment configuration file. See next topic about details of `conda.yaml` file. This is just single file that describes dependencies for all operating systems. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index bbe1317f..a3829bd6 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -33,6 +33,7 @@ and look them thru - what is first error your see in console output, and what is last error your see, then look between +- also look thru all warnings and failures too ## Reporting an issue @@ -50,6 +51,8 @@ - you should share your `robot.yaml` that defines your robot - you should share your code, or minimal sample code, that can reproduce problem you are having +- provide all evidence that you have gathered (as native form as possible, + and as fully as possible; do not truncate stack traces or logs unnecessarily) ## Network access related troubleshooting questions diff --git a/docs/vocabulary.md b/docs/vocabulary.md index c0991298..3be77c64 100644 --- a/docs/vocabulary.md +++ b/docs/vocabulary.md @@ -112,7 +112,7 @@ specifications, but provided for each user as separate space. ## Space -Concrete create environment where processes and robot actually run. Each +Concrete created environment where processes and robot actually run. Each holotree space is identified by three things: user, controller, and space identifier. Each different combination of those values receives their own separate directory. These will each separately consume diskspace. @@ -124,6 +124,9 @@ This is holotree space, that is created by `rcc` but it is not managed by maintain that environment. It can get dirty, can have traditional tooling adding dependencies there, and it can deviate from specification. +Note: unmanaged holotree spaces are not user specific, and managing access +to those spaces is left to tooling/users who use these unmanaged spaces. + ## User User account identity that is using `rcc`. Users wont share concrete holotrees From 5ae4fa248fb5cf708b4512847cbf4e33bc198625 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 14 Nov 2023 07:57:01 +0200 Subject: [PATCH 458/516] Minor fixed (process monitoring) (v17.7.3) - changed subprocess monitoring from 200ms to 550ms, since on slower machines, that 200ms causes too much load (experiment; might need to change later again) --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/running.go | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 37945e35..a8a34b5a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.7.2` + Version = `v17.7.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 09072f0b..e6709315 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.7.3 (date: 14.11.2023) + +- changed subprocess monitoring from 200ms to 550ms, since on slower machines, + that 200ms causes too much load (experiment; might need to change later again) + ## v17.7.2 (date: 8.11.2023) - documentation updates on maintenance, and vocabulary, etc. diff --git a/operations/running.go b/operations/running.go index 9593ffd9..ca439320 100644 --- a/operations/running.go +++ b/operations/running.go @@ -353,7 +353,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro common.Debug("about to run command - %v", task) journal.CurrentBuildEvent().RobotStarts() - pipe := WatchChildren(os.Getpid(), 200*time.Millisecond) + pipe := WatchChildren(os.Getpid(), 550*time.Millisecond) shell.WithInterrupt(func() { exitcode := 0 if common.NoOutputCapture { From 8771d622443efae2aa04c2d8c85b5b5c2e7aa3d6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 14 Nov 2023 09:44:35 +0200 Subject: [PATCH 459/516] Diagnose missing drift file (v17.8.0) - expanded documentation on `rcc robot dependencies` command - added warning when developer declared dependencies file is missing, and when environment configuration drift is shown - added diagnostics to detect missing dependencies drift file --- cmd/robotdependencies.go | 18 +++++++++++++++--- common/categories.go | 1 + common/version.go | 2 +- conda/dependencies.go | 4 ++++ docs/changelog.md | 7 +++++++ operations/diagnostics.go | 6 ++++++ robot/robot.go | 6 +++++- 7 files changed, 39 insertions(+), 5 deletions(-) diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index 762a1db7..f3b96368 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -34,9 +34,21 @@ func doCopyDependencies(config robot.Robot, label string) { } var robotDependenciesCmd = &cobra.Command{ - Use: "dependencies", - Short: "View wanted vs. available dependencies of robot execution environment.", - Long: "View wanted vs. available dependencies of robot execution environment.", + Use: "dependencies", + Short: "View wanted vs. available dependencies of robot execution environment.", + Long: `View wanted vs. available dependencies of robot execution environment. + +When developers capture this desired environment state in their automation +development phase, it can later help resolving problems in production when +dependencies have drifted away from original desire. + +This information can be viewed with this command, and will also be available +when automation is executed using various "rcc run" commands. + +Examples: + rcc robot dependencies --export # to export/refresh set of dependencies + rcc robot dependencies # to view drift information agains specifi space +`, Aliases: []string{"deps"}, Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { diff --git a/common/categories.go b/common/categories.go index 1e60a6b7..16a363b3 100644 --- a/common/categories.go +++ b/common/categories.go @@ -17,4 +17,5 @@ const ( CategoryNetworkTLSVerify = 4060 CategoryNetworkTLSChain = 4070 CategoryEnvironmentCache = 5010 + CategoryRobotDriftfile = 6010 ) diff --git a/common/version.go b/common/version.go index a8a34b5a..70737a34 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.7.3` + Version = `v17.8.0` ) diff --git a/conda/dependencies.go b/conda/dependencies.go index 6bd78631..59b3ab3d 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -202,6 +202,10 @@ func SideBySideViewOfDependencies(goldenfile, wantedfile string) (err error) { if !hasgold { return fmt.Errorf("Running against old environment, which does not have 'golden-ee.yaml' file.") } + if len(want) == 0 { + pretty.Note("There was no developer declared dependency file, so could not show actual configuration drift.") + pretty.Highlight("Ask developer to fix that by running `rcc robot dependencies --export` command in their desired environment.") + } return nil } diff --git a/docs/changelog.md b/docs/changelog.md index e6709315..d3ffc1c6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v17.8.0 (date: 14.11.2023) + +- expanded documentation on `rcc robot dependencies` command +- added warning when developer declared dependencies file is missing, and + when environment configuration drift is shown +- added diagnostics to detect missing dependencies drift file + ## v17.7.3 (date: 14.11.2023) - changed subprocess monitoring from 200ms to 550ms, since on slower machines, diff --git a/operations/diagnostics.go b/operations/diagnostics.go index d3bca940..9565f279 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -641,6 +641,12 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str } func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { + dependencies := robot.DependenciesFilename(rootdir) + if !pathlib.IsFile(dependencies) { + diagnose := target.Diagnose("Robot") + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + diagnose.Warning(common.CategoryRobotDriftfile, supportGeneralUrl, "Dependencies drift file %q is missing!", dependencies) + } jsons := pathlib.RecursiveGlob(rootdir, "*.json") diagnoseFilesUnmarshal(json.Unmarshal, "JSON", rootdir, jsons, target) yamls := pathlib.RecursiveGlob(rootdir, "*.yaml") diff --git a/robot/robot.go b/robot/robot.go index d993be0b..e1592408 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -265,8 +265,12 @@ func (it *robot) Validate() (bool, error) { return true, nil } +func DependenciesFilename(root string) string { + return filepath.Join(root, "dependencies.yaml") +} + func (it *robot) DependenciesFile() (string, bool) { - filename := filepath.Join(it.Root, "dependencies.yaml") + filename := DependenciesFilename(it.Root) return filename, pathlib.IsFile(filename) } From c607f7af9d9e2502ca16966005848e2a97512c06 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 14 Nov 2023 11:50:11 +0200 Subject: [PATCH 460/516] ROBOCORP_HOME related fixes (v17.8.1) - bug fix: made check of users sharing `ROBOCORP_HOME` case insenstive - added note on `ROBOCORP_HOME` permissions into documentation - also `journal.run` has event when multiple users share same home --- cmd/rcc/main.go | 2 +- common/version.go | 2 +- docs/changelog.md | 6 ++++++ docs/recipes.md | 2 ++ operations/cache.go | 12 ++++++++++++ operations/running.go | 9 +++++++++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 04f3ab4f..daa23a6c 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -41,7 +41,7 @@ func EnsureUserRegistered() (string, error) { if err != nil { return warning, err } - updated, ok := set.Update(cache.Users, who.Username) + updated, ok := set.Update(cache.Users, strings.ToLower(who.Username)) size := len(updated) if size > 1 { warning = fmt.Sprintf("More than one user is using same ROBOCORP_HOME location! Those users are: %s!", strings.Join(updated, ", ")) diff --git a/common/version.go b/common/version.go index 70737a34..4a0c9755 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.8.0` + Version = `v17.8.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index d3ffc1c6..88f480f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v17.8.1 (date: 14.11.2023) + +- bug fix: made check of users sharing `ROBOCORP_HOME` case insenstive +- added note on `ROBOCORP_HOME` permissions into documentation +- also `journal.run` has event when multiple users share same home + ## v17.8.0 (date: 14.11.2023) - expanded documentation on `rcc robot dependencies` command diff --git a/docs/recipes.md b/docs/recipes.md index d57c24ce..b00a6b86 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -383,6 +383,8 @@ that somewhere else, then this environment variable does the trick. and operate on - never put `ROBOCORP_HOME` on network drive, since those tend to be slow, and using those can cause real performance issues +- always make sure, that user owning that `ROBOCORP_HOME` directory has full + control access and permissions to everything inside that directory structure ### When you might actually need to setup `ROBOCORP_HOME`? diff --git a/operations/cache.go b/operations/cache.go index 7068e805..9e007dc4 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -4,9 +4,11 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/set" "github.com/robocorp/rcc/xviper" "gopkg.in/yaml.v2" @@ -37,10 +39,20 @@ func (it Cache) Ready() *Cache { } if it.Users == nil { it.Users = []string{} + } else { + it.Users = it.Userset() } return &it } +func (it Cache) Userset() []string { + result := make([]string, 0, len(it.Users)) + for _, username := range it.Users { + result, _ = set.Update(result, strings.ToLower(username)) + } + return result +} + func cacheLockFile() string { return fmt.Sprintf("%s.lck", cacheLocation()) } diff --git a/operations/running.go b/operations/running.go index ca439320..3e6b3970 100644 --- a/operations/running.go +++ b/operations/running.go @@ -176,7 +176,16 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. pretty.Exit(4, "Error: this robot requires holotree, but no --space was given!") } + pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) journal.ForRun(filepath.Join(config.ArtifactDirectory(), "journal.run")) + cache, err := SummonCache() + if err == nil && len(cache.Userset()) > 1 { + pretty.Note("There seems to be multiple users sharing ROBOCORP_HOME, which might cause problems.") + pretty.Note("These are the users: %s.", cache.Userset()) + pretty.Highlight("To correct this problem, make sure that there is only one user per ROBOCORP_HOME.") + common.RunJournal("sharing", fmt.Sprintf("name=%s from=%s users=%s", theTask, packfile, cache.Userset()), "multiple users shareing ROBOCORP_HOME") + } + common.RunJournal("start task", fmt.Sprintf("name=%s from=%s", theTask, packfile), "at task environment setup") if !config.UsesConda() { From 6c98332b8bd0b0c552560ca818948c19d61e08fc Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 15 Nov 2023 09:54:00 +0200 Subject: [PATCH 461/516] rcc version check (v17.9.0) - rcc is now checking if newer released versions are available, and adds notification into stderr if not using that version --- cmd/rcc/main.go | 5 ++ cmd/root.go | 1 + common/version.go | 2 +- docs/changelog.md | 5 ++ operations/rccversioncheck.go | 103 ++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 operations/rccversioncheck.go diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index daa23a6c..64ffb3ac 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -125,6 +125,11 @@ func markTempForRecycling() { func main() { defer ExitProtection() + notify := operations.RccVersionCheck() + if notify != nil { + defer notify() + } + warning, _ := EnsureUserRegistered() if len(warning) > 0 { defer pretty.Warning("%s", warning) diff --git a/cmd/root.go b/cmd/root.go index 184c9c77..80b27b60 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -95,6 +95,7 @@ func Execute() { }() rootCmd.SetArgs(os.Args[1:]) + err := rootCmd.Execute() pretty.Guard(err == nil, 1, "Error: [rcc %v] %v", common.Version, err) } diff --git a/common/version.go b/common/version.go index 4a0c9755..f9a4a6a0 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.8.1` + Version = `v17.9.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 88f480f2..878dccd5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.9.0 (date: 15.11.2023) + +- rcc is now checking if newer released versions are available, and adds + notification into stderr if not using that version + ## v17.8.1 (date: 14.11.2023) - bug fix: made check of users sharing `ROBOCORP_HOME` case insenstive diff --git a/operations/rccversioncheck.go b/operations/rccversioncheck.go new file mode 100644 index 00000000..72ee20f7 --- /dev/null +++ b/operations/rccversioncheck.go @@ -0,0 +1,103 @@ +package operations + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/robocorp/rcc/cloud" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/settings" +) + +type ( + rccVersions struct { + Tested []*versionInfo `json:"tested"` + } + versionInfo struct { + Version string `json:"version"` + When string `json:"when"` + } +) + +func rccReleaseInfoURL() string { + // https://downloads.robocorp.com/rcc/releases/index.json + return settings.Global.DownloadsLink("/rcc/releases/index.json") +} + +func rccVersionsJsonPart() string { + return filepath.Join(common.TemplateLocation(), "rcc.json.part") +} + +func rccVersionsJson() string { + return filepath.Join(common.TemplateLocation(), "rcc.json") +} + +func updateRccVersionInfo() (err error) { + defer fail.Around(&err) + + if !needNewRccInfo() { + return nil + } + return downloadVersionsJson() +} + +func needNewRccInfo() bool { + stat, err := os.Stat(rccVersionsJson()) + return err != nil || common.DayCountSince(stat.ModTime()) > 2 +} + +func downloadVersionsJson() (err error) { + defer fail.Around(&err) + + sourceURL := rccReleaseInfoURL() + partfile := rccVersionsJsonPart() + err = cloud.Download(sourceURL, partfile) + fail.On(err != nil, "Failure loading %q, reason: %s", sourceURL, err) + finaltarget := rccVersionsJson() + os.Remove(finaltarget) + return os.Rename(partfile, finaltarget) +} + +func loadVersionsInfo() (versions *rccVersions, err error) { + defer fail.Around(&err) + + blob, err := os.ReadFile(rccVersionsJson()) + fail.Fast(err) + versions = &rccVersions{} + err = json.Unmarshal(blob, versions) + fail.Fast(err) + return versions, nil +} + +func pickLatestTestedVersion(versions *rccVersions) (uint64, string, string) { + highest, text, when := uint64(0), "unknown", "unkown" + for _, version := range versions.Tested { + number, _ := conda.AsVersion(version.Version) + if number > highest { + text = version.Version + when = version.When + highest = number + } + } + return highest, text, when +} + +func RccVersionCheck() func() { + updateRccVersionInfo() + versions, err := loadVersionsInfo() + if err != nil || versions == nil { + return nil + } + tested, textual, when := pickLatestTestedVersion(versions) + current, _ := conda.AsVersion(common.Version) + if tested == 0 || current == 0 || current >= tested { + return nil + } + return func() { + pretty.Note("Now running rcc %s. There is newer rcc version %s available, released at %s.", common.Version, textual, when) + } +} From 72f713a8fea27496aef25ef7c17c437aa2be3d88 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 16 Nov 2023 12:46:57 +0200 Subject: [PATCH 462/516] temp and pyc management (v17.10.0) - functionality to tell rcc to not manage anything relating to temporaray directories (that is, something else is managing those) - new environment variable `RCC_NO_TEMP_MANAGEMENT` and new command line flag `--no-temp-management` to control above thing - functionality to tell rcc to not manage anything relating to python .pyc files (that is, something else is managing those) - new environment variable `RCC_NO_PYC_MANAGEMENT` and new command line flag `--no-pyc-management` to control above thing - added diagnostics warnings when above environment variables are set --- cmd/rcc/main.go | 8 ++++++++ cmd/root.go | 2 ++ common/categories.go | 1 + common/variables.go | 24 +++++++++++++++++++++++- common/version.go | 2 +- conda/platform_darwin.go | 8 +++++--- conda/platform_linux.go | 8 +++++--- conda/platform_windows.go | 8 +++++--- conda/robocorp.go | 20 ++++++++++++++++---- docs/changelog.md | 12 ++++++++++++ docs/recipes.md | 9 +++++++++ operations/diagnostics.go | 27 +++++++++++++++++++++++++++ 12 files changed, 114 insertions(+), 15 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 64ffb3ac..dcb2a595 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -92,6 +92,10 @@ func ExitProtection() { } func startTempRecycling() { + if common.DisableTempManagement() { + common.Timeline("temp management disabled -- no temp recycling") + return + } defer common.Timeline("temp recycling done") pattern := filepath.Join(common.RobocorpTempRoot(), "*", "recycle.now") found, err := filepath.Glob(pattern) @@ -110,6 +114,10 @@ func startTempRecycling() { } func markTempForRecycling() { + if common.DisableTempManagement() { + common.Timeline("temp management disabled -- temp not marked for recycling") + return + } if markedAlready { return } diff --git a/cmd/root.go b/cmd/root.go index 80b27b60..b78c5d0c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -126,6 +126,8 @@ func init() { rootCmd.PersistentFlags().IntVarP(&anywork.WorkerCount, "workers", "", 0, "scale background workers manually (do not use, unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.UnmanagedSpace, "unmanaged", "", false, "work with unmanaged holotree spaces, DO NOT USE (unless you know what you are doing)") rootCmd.PersistentFlags().BoolVarP(&common.WarrantyVoidedFlag, "warranty-voided", "", false, "experimental, warranty voided, dangerous mode ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.NoTempManagement, "no-temp-management", "", false, "rcc wont do any temp directory management ... DO NOT USE (unless you know what you are doing)") + rootCmd.PersistentFlags().BoolVarP(&common.NoPycManagement, "no-pyc-management", "", false, "rcc wont do any .pyc file management ... DO NOT USE (unless you know what you are doing)") } func initConfig() { diff --git a/common/categories.go b/common/categories.go index 16a363b3..981728cf 100644 --- a/common/categories.go +++ b/common/categories.go @@ -6,6 +6,7 @@ const ( CategoryLockFile = 1020 CategoryLockPid = 1021 CategoryPathCheck = 1030 + CategoryEnvVarCheck = 1040 CategoryHolotreeShared = 2010 CategoryRobocorpHome = 3010 CategoryRobocorpHomeMembers = 3020 diff --git a/common/variables.go b/common/variables.go index 0cdace9b..dce0e946 100644 --- a/common/variables.go +++ b/common/variables.go @@ -28,6 +28,8 @@ const ( ROBOCORP_HOME_VARIABLE = `ROBOCORP_HOME` RCC_REMOTE_ORIGIN = `RCC_REMOTE_ORIGIN` RCC_REMOTE_AUTHORIZATION = `RCC_REMOTE_AUTHORIZATION` + RCC_NO_TEMP_MANAGEMENT = `RCC_NO_TEMP_MANAGEMENT` + RCC_NO_PYC_MANAGEMENT = `RCC_NO_PYC_MANAGEMENT` VERBOSE_ENVIRONMENT_BUILDING = `RCC_VERBOSE_ENVIRONMENT_BUILDING` ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS = `ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS` RCC_VERBOSITY = `RCC_VERBOSITY` @@ -38,6 +40,8 @@ const ( var ( NoBuild bool + NoTempManagement bool + NoPycManagement bool DeveloperFlag bool StrictFlag bool SharedHolotree bool @@ -66,8 +70,12 @@ func init() { randomIdentifier = fmt.Sprintf("%016x", rand.Uint64()^uint64(os.Getpid())) + lowargs := make([]string, 0, len(os.Args)) + for _, arg := range os.Args { + lowargs = append(lowargs, strings.ToLower(arg)) + } // peek CLI options to pre-initialize "Warranty Voided" indicator - args := set.Set(os.Args) + args := set.Set(lowargs) WarrantyVoidedFlag = set.Member(args, "--warranty-voided") if set.Member(args, "--debug") { verbosity = Debugging @@ -75,6 +83,12 @@ func init() { if set.Member(args, "--trace") { verbosity = Tracing } + if set.Member(args, "--no-temp-management") { + NoTempManagement = true + } + if set.Member(args, "--no-pyc-management") { + NoPycManagement = true + } // Note: HololibCatalogLocation, HololibLibraryLocation and HololibUsageLocation // are force created from "htfs" direcotry.go init function @@ -107,6 +121,14 @@ func RobocorpHome() string { return ExpandPath(defaultRobocorpLocation) } +func DisableTempManagement() bool { + return NoTempManagement || len(os.Getenv(RCC_NO_TEMP_MANAGEMENT)) > 0 +} + +func DisablePycManagement() bool { + return NoPycManagement || len(os.Getenv(RCC_NO_PYC_MANAGEMENT)) > 0 +} + func RccRemoteOrigin() string { return os.Getenv(RCC_REMOTE_ORIGIN) } diff --git a/common/version.go b/common/version.go index f9a4a6a0..1f0f7939 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.9.0` + Version = `v17.10.0` ) diff --git a/conda/platform_darwin.go b/conda/platform_darwin.go index 7659d182..0b5d4f96 100644 --- a/conda/platform_darwin.go +++ b/conda/platform_darwin.go @@ -32,9 +32,11 @@ var ( func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) - tempFolder := common.RobocorpTemp() - env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) - env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + if !common.DisableTempManagement() { + tempFolder := common.RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + } return injectNetworkEnvironment(env) } diff --git a/conda/platform_linux.go b/conda/platform_linux.go index 54134a45..00e1035d 100644 --- a/conda/platform_linux.go +++ b/conda/platform_linux.go @@ -36,9 +36,11 @@ func MicromambaLink() string { func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) - tempFolder := common.RobocorpTemp() - env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) - env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + if !common.DisableTempManagement() { + tempFolder := common.RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + } return injectNetworkEnvironment(env) } diff --git a/conda/platform_windows.go b/conda/platform_windows.go index 01ee1f5b..7f34f4b2 100644 --- a/conda/platform_windows.go +++ b/conda/platform_windows.go @@ -41,9 +41,11 @@ var ( func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.MambaRootPrefix())) - tempFolder := common.RobocorpTemp() - env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) - env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + if !common.DisableTempManagement() { + tempFolder := common.RobocorpTemp() + env = append(env, fmt.Sprintf("TEMP=%s", tempFolder)) + env = append(env, fmt.Sprintf("TMP=%s", tempFolder)) + } return injectNetworkEnvironment(env) } diff --git a/conda/robocorp.go b/conda/robocorp.go index 2e67d3ab..35ccdbd0 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -142,6 +142,22 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st if ok { environment = append(environment, "PYTHON_EXE="+python) } + if !common.DisablePycManagement() { + environment = append(environment, + "PYTHONDONTWRITEBYTECODE=x", + "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), + ) + } else { + common.Timeline(".pyc file management was disabled.") + } + if !common.DisableTempManagement() { + environment = append(environment, + "TEMP="+common.RobocorpTemp(), + "TMP="+common.RobocorpTemp(), + ) + } else { + common.Timeline("temp directory management was disabled.") + } environment = append(environment, "CONDA_DEFAULT_ENV=rcc", "CONDA_PREFIX="+location, @@ -151,8 +167,6 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "PYTHONSTARTUP=", "PYTHONEXECUTABLE=", "PYTHONNOUSERSITE=1", - "PYTHONDONTWRITEBYTECODE=x", - "PYTHONPYCACHEPREFIX="+common.RobocorpTemp(), "ROBOCORP_HOME="+common.RobocorpHome(), "RCC_ENVIRONMENT_HASH="+common.EnvironmentHash, "RCC_INSTALLATION_ID="+xviper.TrackingIdentity(), @@ -160,8 +174,6 @@ func CondaExecutionEnvironment(location string, inject []string, full bool) []st "RCC_TRACKING_ALLOWED="+fmt.Sprintf("%v", xviper.CanTrack()), "RCC_EXE="+common.BinRcc(), "RCC_VERSION="+common.Version, - "TEMP="+common.RobocorpTemp(), - "TMP="+common.RobocorpTemp(), FindPath(location).AsEnvironmental("PATH"), ) environment = append(environment, LoadActivationEnvironment(location)...) diff --git a/docs/changelog.md b/docs/changelog.md index 878dccd5..24ea9f9f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,17 @@ # rcc change log +## v17.10.0 (date: 16.11.2023) + +- functionality to tell rcc to not manage anything relating to temporaray + directories (that is, something else is managing those) +- new environment variable `RCC_NO_TEMP_MANAGEMENT` and new command line flag + `--no-temp-management` to control above thing +- functionality to tell rcc to not manage anything relating to python .pyc + files (that is, something else is managing those) +- new environment variable `RCC_NO_PYC_MANAGEMENT` and new command line flag + `--no-pyc-management` to control above thing +- added diagnostics warnings when above environment variables are set + ## v17.9.0 (date: 15.11.2023) - rcc is now checking if newer released versions are available, and adds diff --git a/docs/recipes.md b/docs/recipes.md index b00a6b86..8570f0cb 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -459,6 +459,15 @@ rcc holotree init --revoke - `RCC_VERBOSITY` controls how verbose rcc output will be. If this variable is not set, then verbosity is taken from `--silent`, `--debug`, and `--trace` CLI flags. Valid values for this variable are `silent`, `debug` and `trace`. +- `RCC_NO_TEMP_MANAGEMENT` with any non-empty value will prevent rcc for + doing any management in relation to temporary directories; using this + environment variable means, that something else is managing temporary + directories life cycles (and this might also break environment isolation) +- `RCC_NO_PYC_MANAGEMENT` with any non-empty value will prevent rcc for + doing any .pyc file management; using this environment variable means, that + something else is doing that management (and using this makes rcc slower + and hololibs become bigger and grow faster, since .pyc files are unfriendly + to caching) ## How to troubleshoot rcc setup and robots? diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 9565f279..b0eea7e1 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -112,6 +112,8 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Details["ENV:SHELL"] = os.Getenv("SHELL") result.Details["ENV:LANG"] = os.Getenv("LANG") result.Details["warranty-voided-mode"] = fmt.Sprintf("%v", common.WarrantyVoided()) + result.Details["temp-management-disabled"] = fmt.Sprintf("%v", common.DisableTempManagement()) + result.Details["pyc-management-disabled"] = fmt.Sprintf("%v", common.DisablePycManagement()) for name, filename := range lockfiles() { result.Details[name] = filename @@ -152,6 +154,10 @@ func runDiagnostics(quick bool) *common.DiagnosticStatus { result.Checks = append(result.Checks, anyPathCheck("SSL_CERT_FILE")) result.Checks = append(result.Checks, anyPathCheck("WDM_SSL_VERIFY")) + result.Checks = append(result.Checks, anyEnvVarCheck("RCC_NO_TEMP_MANAGEMENT")) + result.Checks = append(result.Checks, anyEnvVarCheck("RCC_NO_PYC_MANAGEMENT")) + result.Checks = append(result.Checks, anyEnvVarCheck("ROBOCORP_OVERRIDE_SYSTEM_REQUIREMENTS")) + if !common.OverrideSystemRequirements() { result.Checks = append(result.Checks, longPathSupportCheck()) } @@ -294,6 +300,27 @@ func lockpidsCheck() []*common.DiagnosticCheck { return result } +func anyEnvVarCheck(key string) *common.DiagnosticCheck { + supportGeneralUrl := settings.Global.DocsLink("troubleshooting") + anyVar := os.Getenv(key) + if len(anyVar) > 0 { + return &common.DiagnosticCheck{ + Type: "OS", + Category: common.CategoryEnvVarCheck, + Status: statusWarning, + Message: fmt.Sprintf("%s is set to %q. This may cause problems.", key, anyVar), + Link: supportGeneralUrl, + } + } + return &common.DiagnosticCheck{ + Type: "OS", + Category: common.CategoryEnvVarCheck, + Status: statusOk, + Message: fmt.Sprintf("%s is not set, which is good.", key), + Link: supportGeneralUrl, + } +} + func anyPathCheck(key string) *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") anyPath := os.Getenv(key) From f4981c5da2d9bade8377c8fa964ff3f097782005 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 23 Nov 2023 12:57:07 +0200 Subject: [PATCH 463/516] Externally managed (PEP 668) (v17.11.0) - adding functionality to mark holotree space as EXTERNALLY-MANAGED (PEP 668) --- .gitignore | 1 + Rakefile | 2 +- assets/externally_managed.txt | 21 +++++++++++++ blobs/asset_test.go | 3 ++ blobs/assets/.gitignore | 1 + blobs/embedded.go | 2 +- cmd/holotree.go | 3 ++ cmd/run.go | 2 +- cmd/task.go | 3 ++ common/variables.go | 1 + common/version.go | 2 +- conda/extarnallymanaged.go | 55 +++++++++++++++++++++++++++++++++++ docs/changelog.md | 4 +++ htfs/commands.go | 17 ++++++----- 14 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 assets/externally_managed.txt create mode 100644 conda/extarnallymanaged.go diff --git a/.gitignore b/.gitignore index 6df2bba6..04877b44 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ rcc.yaml rcccache.yaml tags .DS_Store +.use diff --git a/Rakefile b/Rakefile index 8f3532b4..e1428d7b 100644 --- a/Rakefile +++ b/Rakefile @@ -52,7 +52,7 @@ task :assets => [:noassets, :micromamba] do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - cp FileList['assets/micromamba_version.txt'], 'blobs/assets/' + cp FileList['assets/*.txt'], 'blobs/assets/' cp FileList['assets/*.yaml'], 'blobs/assets/' cp FileList['assets/man/*.txt'], 'blobs/assets/man/' cp FileList['docs/*.md'], 'blobs/docs/' diff --git a/assets/externally_managed.txt b/assets/externally_managed.txt new file mode 100644 index 00000000..62350831 --- /dev/null +++ b/assets/externally_managed.txt @@ -0,0 +1,21 @@ +[externally-managed] +Error=by Robocorp tooling called `rcc`. + + To install Python packages into this managed environment, those should + be added to `conda.yaml` file, or more generally into "environment + configuration files". + + Motivation with these kind of managed environments is, that they provide: + - repeatability, so that same environment can be recreated later + - isolation, so that different automations can have their own dependencies + - as few as possible dependency resolutions (currently two: conda and pypi) + - support for "foreign machines", and not just your own personal machine + - supporting Windows, MacOS, and linux operating systems with same automations + + If you don't need above features, or need more flexibility in your personal + developement environment, consider using something else (like virtualenv or + poetry) with your personal tooling, and only use `rcc` managed environments + for final delivery to your users and customers. + + For more details, see: + https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-are-environmentconfigs diff --git a/blobs/asset_test.go b/blobs/asset_test.go index 54b39806..a09bfb27 100644 --- a/blobs/asset_test.go +++ b/blobs/asset_test.go @@ -28,6 +28,9 @@ func TestCanOtherAssets(t *testing.T) { must_be.Panic(func() { blobs.MustAsset("assets/missing.yaml") }) + wont_be.Panic(func() { blobs.MustAsset("assets/micromamba_version.txt") }) + wont_be.Panic(func() { blobs.MustAsset("assets/externally_managed.txt") }) + wont_be.Panic(func() { blobs.MustAsset("assets/templates.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/settings.yaml") }) wont_be.Panic(func() { blobs.MustAsset("assets/speedtest.yaml") }) diff --git a/blobs/assets/.gitignore b/blobs/assets/.gitignore index 64e6f319..68e6ed7b 100644 --- a/blobs/assets/.gitignore +++ b/blobs/assets/.gitignore @@ -1,3 +1,4 @@ *.zip *.yaml +*.txt micromamba* diff --git a/blobs/embedded.go b/blobs/embedded.go index 1eb5ef10..6b5bcce1 100644 --- a/blobs/embedded.go +++ b/blobs/embedded.go @@ -13,7 +13,7 @@ const ( //go:embed assets/*.yaml docs/*.md //go:embed assets/*.zip assets/man/*.txt -//go:embed assets/micromamba_version.txt +//go:embed assets/*.txt var content embed.FS func Asset(name string) ([]byte, error) { diff --git a/cmd/holotree.go b/cmd/holotree.go index 902259e9..331f3d32 100644 --- a/cmd/holotree.go +++ b/cmd/holotree.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -17,4 +18,6 @@ var holotreeCmd = &cobra.Command{ func init() { rootCmd.AddCommand(holotreeCmd) + + holotreeCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") } diff --git a/cmd/run.go b/cmd/run.go index b2182005..dad1daa0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -52,8 +52,8 @@ func captureRunFlags(assistant bool) *operations.RunFlags { } func init() { - taskCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd) + taskCmd.AddCommand(runCmd) runCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") runCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") diff --git a/cmd/task.go b/cmd/task.go index e4fbb04e..8bdda6de 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/common" "github.com/spf13/cobra" ) @@ -14,4 +15,6 @@ executed either locally, or in connection to Robocorp Control Room and tooling.` func init() { rootCmd.AddCommand(taskCmd) + + taskCmd.PersistentFlags().BoolVarP(&common.ExternallyManaged, "externally-managed", "", false, "mark created Python environments as EXTERNALLY-MANAGED (PEP 668)") } diff --git a/common/variables.go b/common/variables.go index dce0e946..63646c20 100644 --- a/common/variables.go +++ b/common/variables.go @@ -42,6 +42,7 @@ var ( NoBuild bool NoTempManagement bool NoPycManagement bool + ExternallyManaged bool DeveloperFlag bool StrictFlag bool SharedHolotree bool diff --git a/common/version.go b/common/version.go index 1f0f7939..85e6c7a9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.10.0` + Version = `v17.11.0` ) diff --git a/conda/extarnallymanaged.go b/conda/extarnallymanaged.go new file mode 100644 index 00000000..5ea1822c --- /dev/null +++ b/conda/extarnallymanaged.go @@ -0,0 +1,55 @@ +package conda + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" +) + +const ( + EXTERNALLY_MANAGED = "EXTERNALLY-MANAGED" +) + +type ( + SysconfigPaths struct { + Stdlib string `json:"stdlib"` + Purelib string `json:"purelib"` + Platlib string `json:"platlib"` + } +) + +func FindSysconfigPaths(path string) (paths *SysconfigPaths, err error) { + defer fail.Around(&err) + + capture, code, err := LiveCapture(path, "python", "-c", "import json, sysconfig; print(json.dumps(sysconfig.get_paths()))") + if err != nil { + common.Fatal(fmt.Sprintf("EXTERNALLY-MANAGED failure [%d/%x]", code, code), err) + return nil, err + } + mappings := &SysconfigPaths{} + err = json.Unmarshal([]byte(capture), mappings) + fail.Fast(err) + return mappings, nil +} + +func ApplyExternallyManaged(path string) (label string, err error) { + defer fail.Around(&err) + + if !common.ExternallyManaged { + return "", nil + } + common.Debug("Applying EXTERNALLY-MANAGED (PEP 668) to environment.") + paths, err := FindSysconfigPaths(path) + fail.Fast(err) + location := filepath.Join(paths.Stdlib, EXTERNALLY_MANAGED) + blob, err := blobs.Asset("assets/externally_managed.txt") + fail.Fast(err) + fail.Fast(os.WriteFile(location, blob, 0o644)) + common.Timeline("applied EXTERNALLY-MANAGED (PEP 668) to this holotree space") + return fmt.Sprintf("%s (PEP 668) ", EXTERNALLY_MANAGED), nil +} diff --git a/docs/changelog.md b/docs/changelog.md index 24ea9f9f..662257f6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.11.0 (date: 23.11.2023) + +- adding functionality to mark holotree space as EXTERNALLY-MANAGED (PEP 668) + ## v17.10.0 (date: 16.11.2023) - functionality to tell rcc to not manage anything relating to temporaray diff --git a/htfs/commands.go b/htfs/commands.go index 29481b23..a3aa3840 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -48,12 +48,12 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal common.Debug("New zipped environment from %q!", holozip) } - path := "" + path, externally := "", "" defer func() { if err != nil { pretty.Regression(15, "Holotree restoration failure, see above [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) } else { - pretty.Progress(15, "Fresh holotree done [with %d workers on %d CPUs].", anywork.Scale(), runtime.NumCPU()) + pretty.Progress(15, "Fresh %sholotree done [with %d workers on %d CPUs].", externally, anywork.Scale(), runtime.NumCPU()) usefile := fmt.Sprintf("%s.use", path) pathlib.AppendFile(usefile, []byte{'.'}) } @@ -83,14 +83,14 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal defer locker.Release() _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") - fail.On(err != nil, "%s", err) + fail.Fast(err) common.EnvironmentHash, common.FreshlyBuildEnvironment = common.BlueprintHash(holotreeBlueprint), false pretty.Progress(2, "Holotree blueprint is %q [%s with %d workers on %d CPUs from %q].", common.EnvironmentHash, common.Platform(), anywork.Scale(), runtime.NumCPU(), filepath.Base(condafile)) journal.CurrentBuildEvent().Blueprint(common.EnvironmentHash) tree, err := New() - fail.On(err != nil, "%s", err) + fail.Fast(err) if !haszip && !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { tree = Virtual() @@ -99,8 +99,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal if common.UnmanagedSpace { tree = Unmanaged(tree) } - err = tree.ValidateBlueprint(holotreeBlueprint) - fail.On(err != nil, "%s", err) + fail.Fast(tree.ValidateBlueprint(holotreeBlueprint)) scorecard = common.NewScorecard() var library Library if haszip { @@ -109,8 +108,7 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal common.Timeline("downgraded to holotree zip library") } else { scorecard.Start() - err = RecordEnvironment(tree, holotreeBlueprint, force, scorecard, puller) - fail.On(err != nil, "%s", err) + fail.Fast(RecordEnvironment(tree, holotreeBlueprint, force, scorecard, puller)) library = tree } @@ -123,6 +121,9 @@ func NewEnvironment(condafile, holozip string, restore, force bool, puller Catal pretty.Progress(14, "Restoring space skipped.") } + externally, err = conda.ApplyExternallyManaged(path) + fail.Fast(err) + return path, scorecard, nil } From 4e00d478f3d5fb02c34b3b3cf4ca5c6b630aed28 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 23 Nov 2023 13:30:01 +0200 Subject: [PATCH 464/516] Revert "Diagnose missing drift file (v17.8.0)" as v17.12.0 This reverts commit 8771d622443efae2aa04c2d8c85b5b5c2e7aa3d6. --- cmd/robotdependencies.go | 18 +++--------------- common/categories.go | 1 - common/version.go | 2 +- conda/dependencies.go | 4 ---- docs/changelog.md | 6 +++++- operations/diagnostics.go | 6 ------ robot/robot.go | 6 +----- 7 files changed, 10 insertions(+), 33 deletions(-) diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index f3b96368..762a1db7 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -34,21 +34,9 @@ func doCopyDependencies(config robot.Robot, label string) { } var robotDependenciesCmd = &cobra.Command{ - Use: "dependencies", - Short: "View wanted vs. available dependencies of robot execution environment.", - Long: `View wanted vs. available dependencies of robot execution environment. - -When developers capture this desired environment state in their automation -development phase, it can later help resolving problems in production when -dependencies have drifted away from original desire. - -This information can be viewed with this command, and will also be available -when automation is executed using various "rcc run" commands. - -Examples: - rcc robot dependencies --export # to export/refresh set of dependencies - rcc robot dependencies # to view drift information agains specifi space -`, + Use: "dependencies", + Short: "View wanted vs. available dependencies of robot execution environment.", + Long: "View wanted vs. available dependencies of robot execution environment.", Aliases: []string{"deps"}, Run: func(cmd *cobra.Command, args []string) { if common.DebugFlag() { diff --git a/common/categories.go b/common/categories.go index 981728cf..a07ac0a3 100644 --- a/common/categories.go +++ b/common/categories.go @@ -18,5 +18,4 @@ const ( CategoryNetworkTLSVerify = 4060 CategoryNetworkTLSChain = 4070 CategoryEnvironmentCache = 5010 - CategoryRobotDriftfile = 6010 ) diff --git a/common/version.go b/common/version.go index 85e6c7a9..e7480d9f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.11.0` + Version = `v17.12.0` ) diff --git a/conda/dependencies.go b/conda/dependencies.go index 59b3ab3d..6bd78631 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -202,10 +202,6 @@ func SideBySideViewOfDependencies(goldenfile, wantedfile string) (err error) { if !hasgold { return fmt.Errorf("Running against old environment, which does not have 'golden-ee.yaml' file.") } - if len(want) == 0 { - pretty.Note("There was no developer declared dependency file, so could not show actual configuration drift.") - pretty.Highlight("Ask developer to fix that by running `rcc robot dependencies --export` command in their desired environment.") - } return nil } diff --git a/docs/changelog.md b/docs/changelog.md index 662257f6..78846dba 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v17.12.0 (date: 23.11.2023) + +- reverted changes done in v17.8.0 (git hash 8771d622443efae2aa04c2d8c85b5b5c2e7aa3d6) + ## v17.11.0 (date: 23.11.2023) - adding functionality to mark holotree space as EXTERNALLY-MANAGED (PEP 668) @@ -27,7 +31,7 @@ - added note on `ROBOCORP_HOME` permissions into documentation - also `journal.run` has event when multiple users share same home -## v17.8.0 (date: 14.11.2023) +## v17.8.0 (date: 14.11.2023) REVERTED - expanded documentation on `rcc robot dependencies` command - added warning when developer declared dependencies file is missing, and diff --git a/operations/diagnostics.go b/operations/diagnostics.go index b0eea7e1..1ce7a869 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -668,12 +668,6 @@ func diagnoseFilesUnmarshal(tool Unmarshaler, label, rootdir string, paths []str } func addFileDiagnostics(rootdir string, target *common.DiagnosticStatus) { - dependencies := robot.DependenciesFilename(rootdir) - if !pathlib.IsFile(dependencies) { - diagnose := target.Diagnose("Robot") - supportGeneralUrl := settings.Global.DocsLink("troubleshooting") - diagnose.Warning(common.CategoryRobotDriftfile, supportGeneralUrl, "Dependencies drift file %q is missing!", dependencies) - } jsons := pathlib.RecursiveGlob(rootdir, "*.json") diagnoseFilesUnmarshal(json.Unmarshal, "JSON", rootdir, jsons, target) yamls := pathlib.RecursiveGlob(rootdir, "*.yaml") diff --git a/robot/robot.go b/robot/robot.go index e1592408..d993be0b 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -265,12 +265,8 @@ func (it *robot) Validate() (bool, error) { return true, nil } -func DependenciesFilename(root string) string { - return filepath.Join(root, "dependencies.yaml") -} - func (it *robot) DependenciesFile() (string, bool) { - filename := DependenciesFilename(it.Root) + filename := filepath.Join(it.Root, "dependencies.yaml") return filename, pathlib.IsFile(filename) } From d5e3b15f41d22655a8f901a999d02692b6a5f22b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 27 Nov 2023 11:25:44 +0200 Subject: [PATCH 465/516] Bugfix: PATH duplicates and holotrees (v17.12.1) - bugfix: removing duplicates and existing holotree from PATHs before adding new items in PATH --- common/variables.go | 12 ++++++++---- common/version.go | 2 +- docs/changelog.md | 5 +++++ pathlib/targetpath.go | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/common/variables.go b/common/variables.go index 63646c20..b3921ea0 100644 --- a/common/variables.go +++ b/common/variables.go @@ -381,10 +381,7 @@ func ensureDirectory(name string) { } } -func UserHomeIdentity() string { - if UnmanagedSpace { - return "UNMNGED" - } +func SymbolicUserIdentity() string { location, err := os.UserHomeDir() if err != nil { return "badcafe" @@ -392,3 +389,10 @@ func UserHomeIdentity() string { digest := fmt.Sprintf("%02x", Siphash(9007799254740993, 2147487647, []byte(location))) return digest[:7] } + +func UserHomeIdentity() string { + if UnmanagedSpace { + return "UNMNGED" + } + return SymbolicUserIdentity() +} diff --git a/common/version.go b/common/version.go index e7480d9f..b80fed4c 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v17.12.0` + Version = `v17.12.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 78846dba..96bc0343 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v17.12.1 (date: 27.11.2023) + +- bugfix: removing duplicates and existing holotree from PATHs before adding + new items in PATH + ## v17.12.0 (date: 23.11.2023) - reverted changes done in v17.8.0 (git hash 8771d622443efae2aa04c2d8c85b5b5c2e7aa3d6) diff --git a/pathlib/targetpath.go b/pathlib/targetpath.go index 69bc7fb2..241c52b5 100644 --- a/pathlib/targetpath.go +++ b/pathlib/targetpath.go @@ -4,13 +4,44 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" + + "github.com/robocorp/rcc/common" ) type PathParts []string +func noDuplicates(paths PathParts) PathParts { + seen := make(map[string]bool) + result := make(PathParts, 0, len(paths)) + for _, part := range paths { + if seen[part] { + continue + } + result = append(result, part) + seen[part] = true + } + return result +} + +func noPreviousHolotrees(paths PathParts) PathParts { + form := fmt.Sprintf("\\b(?:%s|UNMNGED)_[0-9a-f]{7}_[0-9a-f]{8}\\b", common.SymbolicUserIdentity()) + pattern, err := regexp.Compile(form) + if err != nil { + return paths + } + result := make(PathParts, 0, len(paths)) + for _, part := range paths { + if !pattern.MatchString(part) { + result = append(result, part) + } + } + return result +} + func TargetPath() PathParts { - return filepath.SplitList(os.Getenv("PATH")) + return noPreviousHolotrees(noDuplicates(filepath.SplitList(os.Getenv("PATH")))) } func PathFrom(parts ...string) PathParts { From 19a9360fb11a77c9b031d9cf551de30cde818d20 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Tue, 9 Jan 2024 17:10:42 +0200 Subject: [PATCH 466/516] Update README.md --- README.md | 56 +++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 51fd9883..ad0bf75e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ ![RCC](/docs/title.png) -RCC is a set of tooling that allows you to create, manage, and distribute Python-based self-contained automation packages - or robots :robot: as we call them. And run them on soft-containers that have access to rest of your machine. +RCC allows you to create, manage, and distribute Python-based self-contained automation packages. RCC also allows you to run your automations in isolated Python environments so they can still access the rest of your machine. -Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) configuration file, `rcc` is a foundation that allows anyone to build and share automation with ease. +🚀 "Repeatable, movable and isolated Python environments for your automation." + +Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) configuration file, `rcc` is a foundation that allows anyone to build and share automation easily.

7vb*K@TLS`f5^f1{f6JI!L!r%a$^T$m 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 z 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 DfTdq< diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 3f8b4264..40d685d1 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -29,7 +29,7 @@ func ZipLibrary(zipfile string) (Library, error) { for _, entry := range content.File { lookup[entry.Name] = entry } - identity := strings.ToLower(fmt.Sprintf("%s %s %q", runtime.GOOS, runtime.GOARCH, common.RobocorpHome())) + identity := strings.ToLower(fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)) return &ziplibrary{ content: content, identity: sipit([]byte(identity)), @@ -69,7 +69,7 @@ func (it *ziplibrary) Open(digest string) (readable io.Reader, closer Closer, er } func (it *ziplibrary) CatalogPath(key string) string { - return filepath.Join("catalog", fmt.Sprintf("%s.%016x", key, it.identity)) + return filepath.Join("catalog", fmt.Sprintf("%s.%s", key, Platform())) } func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err error) { From 089bd4d3b5750fbe22d30fbc62eb62466c597b74 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 3 Jun 2021 13:36:22 +0300 Subject: [PATCH 141/516] RCC-180: exporting catalogs from holotree (v9.18.0) - now using holotree location from catalog, so that catalog decides where holotree is created (defaults to `ROBOCORP_HOME` but can be different) - if hololib.zip exist, then `--space` flag must be given or run fails - hololib.zip is now reported in robot diagnostics - environment difference print is now (mostly) behind `--trace` flag - if rcc is not interactive, color toggling on Windows is skipped - micromamba download is now done "on demand" only - added robot tests for hololib.zip workflow --- cmd/assistantRun.go | 5 -- cmd/carrier.go | 5 -- cmd/cloudPrepare.go | 2 - cmd/envNew.go | 4 -- cmd/holotreeBootstrap.go | 4 -- cmd/holotreeVariables.go | 3 - cmd/run.go | 6 -- cmd/shell.go | 4 -- cmd/testrun.go | 5 -- cmd/variables.go | 4 -- common/variables.go | 4 ++ common/version.go | 2 +- conda/diagnosis.go | 14 ++-- conda/workflows.go | 3 + docs/changelog.md | 11 +++ htfs/library.go | 12 ++-- operations/running.go | 4 ++ pretty/setup_darwin.go | 2 +- pretty/setup_linux.go | 2 +- pretty/setup_windows.go | 5 +- pretty/variables.go | 2 +- robot/robot.go | 6 ++ robot_tests/export_holozip.robot | 111 +++++++++++++++++++++++++++++++ 23 files changed, 160 insertions(+), 60 deletions(-) create mode 100644 robot_tests/export_holozip.robot diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 74b536e7..7f3bc664 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -8,7 +8,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -30,10 +29,6 @@ var assistantRunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Robot Assistant run lasted").Report() } - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(2, "Could not get micromamba installed.") - } defer xviper.RunMinutes().Done() account := operations.AccountByName(AccountName()) if account == nil { diff --git a/cmd/carrier.go b/cmd/carrier.go index 19550378..ac3bdbd3 100644 --- a/cmd/carrier.go +++ b/cmd/carrier.go @@ -8,7 +8,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -43,10 +42,6 @@ func runCarrier() error { if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } - ok = conda.MustMicromamba() - if !ok { - pretty.Exit(4, "Could not get micromamba installed.") - } defer xviper.RunMinutes().Done() now := time.Now() testrunDir := filepath.Join(".", now.Format("2006-01-02_15_04_05")) diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index e909ef72..82860ff5 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -32,8 +32,6 @@ var prepareCloudCmd = &cobra.Command{ workarea := filepath.Join(os.TempDir(), fmt.Sprintf("workarea%x", common.When)) defer os.RemoveAll(workarea) - pretty.Guard(conda.MustMicromamba(), 1, "Could not get micromamba installed.") - account := operations.AccountByName(AccountName()) pretty.Guard(account != nil, 2, "Could not find account by name: %q", AccountName()) diff --git a/cmd/envNew.go b/cmd/envNew.go index 21013124..7072aab5 100644 --- a/cmd/envNew.go +++ b/cmd/envNew.go @@ -19,10 +19,6 @@ end result will be a composite environment.`, if common.DebugFlag { defer common.Stopwatch("New environment creation lasted").Report() } - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(2, "Could not get micromamba installed.") - } label, err := conda.NewEnvironment(forceFlag, args...) if err != nil { pretty.Exit(1, "Environment creation failed: %v", err) diff --git a/cmd/holotreeBootstrap.go b/cmd/holotreeBootstrap.go index 09e828c2..ecd12e70 100644 --- a/cmd/holotreeBootstrap.go +++ b/cmd/holotreeBootstrap.go @@ -7,7 +7,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" @@ -52,9 +51,6 @@ var holotreeBootstrapCmd = &cobra.Command{ defer common.Stopwatch("Holotree bootstrap lasted").Report() } - ok := conda.MustMicromamba() - pretty.Guard(ok, 1, "Could not get micromamba installed.") - robots := make([]string, 0, 20) for key, _ := range settings.Global.Templates() { zipname := fmt.Sprintf("%s.zip", key) diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 67c3f3bb..a9ea8546 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -81,9 +81,6 @@ var holotreeVariablesCmd = &cobra.Command{ defer common.Stopwatch("Holotree variables command lasted").Report() } - ok := conda.MustMicromamba() - pretty.Guard(ok, 1, "Could not get micromamba installed.") - env := holotreeExpandEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, holotreeForce) if holotreeJson { asJson(env) diff --git a/cmd/run.go b/cmd/run.go index 7d09e7b4..035b71cc 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,9 +3,7 @@ package cmd import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" "github.com/robocorp/rcc/xviper" "github.com/spf13/cobra" @@ -27,10 +25,6 @@ in your own machine.`, if common.DebugFlag { defer common.Stopwatch("Task run lasted").Report() } - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(2, "Could not get micromamba installed.") - } defer xviper.RunMinutes().Done() simple, config, todo, label := operations.LoadTaskWithEnvironment(robotFile, runTask, forceFlag) cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.cli.run", common.Version) diff --git a/cmd/shell.go b/cmd/shell.go index 8c282a6b..6a1f1565 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -21,10 +21,6 @@ command within that environment.`, if common.DebugFlag { defer common.Stopwatch("rcc shell lasted").Report() } - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(2, "Could not get micromamba installed.") - } simple, config, todo, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) if simple { pretty.Exit(1, "Cannot do shell for simple execution model.") diff --git a/cmd/testrun.go b/cmd/testrun.go index 40885819..74662744 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -8,7 +8,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/pretty" @@ -27,10 +26,6 @@ var testrunCmd = &cobra.Command{ if common.DebugFlag { defer common.Stopwatch("Task testrun lasted").Report() } - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(4, "Could not get micromamba installed.") - } defer xviper.RunMinutes().Done() now := time.Now() zipfile := filepath.Join(os.TempDir(), fmt.Sprintf("testrun%x.zip", common.When)) diff --git a/cmd/variables.go b/cmd/variables.go index 3de827cc..43115a1e 100644 --- a/cmd/variables.go +++ b/cmd/variables.go @@ -118,10 +118,6 @@ var variablesCmd = &cobra.Command{ Short: "Export environment specific variables as a JSON structure.", Long: "Export environment specific variables as a JSON structure.", Run: func(cmd *cobra.Command, args []string) { - ok := conda.MustMicromamba() - if !ok { - pretty.Exit(2, "Could not get micromamba installed.") - } err := exportEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, jsonFlag) if err != nil { pretty.Exit(1, "Error: Variable exporting failed because: %v", err) diff --git a/common/variables.go b/common/variables.go index 67b17938..99fac04d 100644 --- a/common/variables.go +++ b/common/variables.go @@ -93,6 +93,10 @@ func HolotreeLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "holotree")) } +func UsesHolotree() bool { + return len(HolotreeSpace) > 0 +} + func PipCache() string { return ensureDirectory(filepath.Join(RobocorpHome(), "pipcache")) } diff --git a/common/version.go b/common/version.go index 0bde312b..adeb30e5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.17.2` + Version = `v9.18.0` ) diff --git a/conda/diagnosis.go b/conda/diagnosis.go index 2be7b0b3..f1682d70 100644 --- a/conda/diagnosis.go +++ b/conda/diagnosis.go @@ -54,36 +54,36 @@ func DirhashDiff(history, future map[string]string, warning bool) { sort.Strings(changed) separate := false for _, folder := range removed { - common.Log("- diff: removed %q", folder) + common.Trace("- diff: removed %q", folder) separate = true } if len(changed) > 0 { if separate { - common.Log("-------") + common.Trace("-------") separate = false } for _, folder := range changed { - common.Log("- diff: changed %q", folder) + common.Trace("- diff: changed %q", folder) separate = true } } if len(added) > 0 { if separate { - common.Log("-------") + common.Trace("-------") separate = false } for _, folder := range added { - common.Log("- diff: added %q", folder) + common.Trace("- diff: added %q", folder) separate = true } } if warning { if separate { - common.Log("-------") + common.Trace("-------") separate = false } common.Log("Notice: Robot run modified the environment which will slow down the next run.") - common.Log(" Please inform the robot developer about this.") + common.Log(" Please inform the robot developer about this. Use --trace for details.") } common.Log("---- rcc env diff ----") } diff --git a/conda/workflows.go b/conda/workflows.go index bd568850..81b3ad56 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -142,6 +142,9 @@ func (it InstallObserver) HasFailures(targetFolder string) bool { } func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, error) { + if !MustMicromamba() { + return false, fmt.Errorf("Could not get micromamba installed.") + } targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") common.Timeline("pre cleanup phase.") diff --git a/docs/changelog.md b/docs/changelog.md index cfd1601d..1a190bf1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,16 @@ # rcc change log +## v9.18.0 (date: 3.6.2021) + +- now using holotree location from catalog, so that catalog decides where + holotree is created (defaults to `ROBOCORP_HOME` but can be different) +- if hololib.zip exist, then `--space` flag must be given or run fails +- hololib.zip is now reported in robot diagnostics +- environment difference print is now (mostly) behind `--trace` flag +- if rcc is not interactive, color toggling on Windows is skipped +- micromamba download is now done "on demand" only +- added robot tests for hololib.zip workflow + ## v9.17.2 (date: 2.6.2021) - fixing broken tests, and taking account changed specifications diff --git a/htfs/library.go b/htfs/library.go index 14d7d85e..3a29ffd8 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -268,8 +268,12 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix - metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) - targetdir := filepath.Join(common.HolotreeLocation(), name) + fs, err := NewRoot(it.Stage()) + fail.On(err != nil, "Failed to create stage -> %v", err) + err = fs.LoadFrom(it.CatalogPath(key)) + fail.On(err != nil, "Failed to load catalog %s -> %v", it.CatalogPath(key), err) + metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) + targetdir := filepath.Join(fs.HolotreeBase(), name) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { @@ -280,10 +284,6 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er shadow.Treetop(DigestRecorder(currentstate)) common.Timeline("holotree digest done") } - fs, err := NewRoot(it.Stage()) - fail.On(err != nil, "Failed to create stage -> %v", err) - err = fs.LoadFrom(it.CatalogPath(key)) - fail.On(err != nil, "Failed to load catalog %s -> %v", it.CatalogPath(key), err) err = fs.Relocate(targetdir) fail.On(err != nil, "Failed to relocate %s -> %v", targetdir, err) common.Timeline("holotree make branches start") diff --git a/operations/running.go b/operations/running.go index e8e6947c..5ad2571e 100644 --- a/operations/running.go +++ b/operations/running.go @@ -81,6 +81,10 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. pretty.Exit(3, "Error: Could not resolve task to run. Available tasks are: %v", strings.Join(config.AvailableTasks(), ", ")) } + if config.HasHolozip() && !common.UsesHolotree() { + pretty.Exit(4, "Error: this robot requires holotree, but no --space was given!") + } + if !config.UsesConda() { return true, config, todo, "" } diff --git a/pretty/setup_darwin.go b/pretty/setup_darwin.go index e92ce1bb..3f681537 100644 --- a/pretty/setup_darwin.go +++ b/pretty/setup_darwin.go @@ -1,6 +1,6 @@ package pretty -func localSetup() { +func localSetup(bool) { Iconic = true Disabled = false } diff --git a/pretty/setup_linux.go b/pretty/setup_linux.go index f96a43c2..f3b373db 100644 --- a/pretty/setup_linux.go +++ b/pretty/setup_linux.go @@ -1,6 +1,6 @@ package pretty -func localSetup() { +func localSetup(bool) { Iconic = false Disabled = false } diff --git a/pretty/setup_windows.go b/pretty/setup_windows.go index 02445cf2..31cfafdf 100644 --- a/pretty/setup_windows.go +++ b/pretty/setup_windows.go @@ -12,9 +12,12 @@ const ( ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 ) -func localSetup() { +func localSetup(interactive bool) { Iconic = false Disabled = true + if !interactive { + return + } kernel32 := syscall.NewLazyDLL("kernel32.dll") if kernel32 == nil { common.Trace("Cannot use colors. Did not get kernel32.dll!") diff --git a/pretty/variables.go b/pretty/variables.go index 4db330b2..1e6ffaeb 100644 --- a/pretty/variables.go +++ b/pretty/variables.go @@ -30,7 +30,7 @@ func Setup() { stderr := isatty.IsTerminal(os.Stderr.Fd()) Interactive = stdin && stdout && stderr - localSetup() + localSetup(Interactive) common.Trace("Interactive mode enabled: %v; colors enabled: %v; icons enabled: %v", Interactive, !Disabled, Iconic) if Interactive && !Disabled && !Colorless { diff --git a/robot/robot.go b/robot/robot.go index 414f65fe..1a91a99c 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -27,6 +27,7 @@ type Robot interface { CondaConfigFile() string CondaHash() string RootDirectory() string + HasHolozip() bool Holozip() string Validate() (bool, error) Diagnostics(*common.DiagnosticStatus, bool) @@ -161,6 +162,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) target.Details["robot-conda-file"] = it.CondaConfigFile() target.Details["robot-conda-hash"] = it.CondaHash() + target.Details["hololib.zip"] = it.Holozip() plan, ok := conda.InstallationPlan(it.CondaHash()) if ok { target.Details["robot-conda-plan"] = plan @@ -205,6 +207,10 @@ func (it *robot) RootDirectory() string { return it.Root } +func (it *robot) HasHolozip() bool { + return len(it.Holozip()) > 0 +} + func (it *robot) Holozip() string { zippath := filepath.Join(it.Root, "hololib.zip") if pathlib.IsFile(zippath) { diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot new file mode 100644 index 00000000..34476eff --- /dev/null +++ b/robot_tests/export_holozip.robot @@ -0,0 +1,111 @@ +*** Settings *** +Library OperatingSystem +Library supporting.py +Resource resources.robot +Suite Setup Export setup +Suite Teardown Export teardown + +*** Keywords *** +Export setup + Remove Directory tmp/developer True + Remove Directory tmp/guest True + Remove Directory tmp/standalone True + +Export teardown + Set Environment Variable ROBOCORP_HOME tmp/robocorp + Remove Directory tmp/developer True + Remove Directory tmp/guest True + Remove Directory tmp/standalone True + +*** Test cases *** + +Workflow with hololib.zip export + Set Environment Variable ROBOCORP_HOME tmp/developer + + Goal Create extended robot into tmp/standalone folder using force. + Step build/rcc robot init --controller citests -t extended -d tmp/standalone -f + Use STDERR + Must Have OK. + + Goal Create environment for standalone robot + Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml + Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_INSTALLATION_ID= + Must Have 4e67cd8d4_fcb4b859 + Use STDERR + Must Have Downloading micromamba + Must Have Progress: 4/6 + Must Have Progress: 6/6 + + Goal Must have author space visible + Step build/rcc ht ls + Use STDERR + Must Have 4e67cd8d4_fcb4b859 + Must Have rcc.citests + Must Have author + Must Have f130d7d72d4d4663 + Wont Have guest + + Goal Show exportable environment list + Step build/rcc ht export + Use STDERR + Must Have Selectable catalogs + Must Have - f130d7d72d4d4663 + Must Have OK. + + Goal Export environment for standalone robot + Step build/rcc ht export -z tmp/standalone/hololib.zip f130d7d72d4d4663 + Use STDERR + Wont Have Selectable catalogs + Must Have OK. + + Goal Wrap the robot + Step build/rcc robot wrap -z tmp/full.zip -d tmp/standalone/ + Use STDERR + Must Have OK. + + Goal See contents of that robot + Step unzip -v tmp/full.zip + Must Have robot.yaml + Must Have conda.yaml + Must Have hololib.zip + + Goal Can delete author space + Step build/rcc ht delete 4e67cd8d4_fcb4b859 + Step build/rcc ht ls + Use STDERR + Wont Have 4e67cd8d4_fcb4b859 + Wont Have rcc.citests + Wont Have author + Wont Have f130d7d72d4d4663 + Wont Have guest + + Set Environment Variable ROBOCORP_HOME tmp/guest + + Goal Can run as guest + Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' + Use STDERR + Wont Have Downloading micromamba + Must Have OK. + + Goal No spaces created under guest + Step build/rcc ht ls + Use STDERR + Wont Have 4e67cd8d4_fcb4b859 + Wont Have rcc.citests + Wont Have author + Wont Have f130d7d72d4d4663 + Wont Have 4e67cd8d4_559e19be + Wont Have guest + + Set Environment Variable ROBOCORP_HOME tmp/developer + + Goal Space created under author for guest + Step build/rcc ht ls + Use STDERR + Wont Have 4e67cd8d4_fcb4b859 + Wont Have author + Must Have rcc.citests + Must Have f130d7d72d4d4663 + Must Have 4e67cd8d4_aacf1552 + Must Have guest From 9bd1883840a9c760dbcd86fd6ead6df45b9b234a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Jun 2021 11:32:09 +0300 Subject: [PATCH 142/516] RCC-181: event journaling (v9.19.0) - added event journaling support (no user visible yet) - added first event "space-used" in holotree restore operations (this enables tracking of all places where environments are created) --- anywork/worker_test.go | 1 + common/variables.go | 4 +++ common/version.go | 2 +- docs/changelog.md | 6 +++++ fail/fail_test.go | 1 + htfs/library.go | 7 ++++-- htfs/virtual.go | 2 ++ htfs/ziplibrary.go | 2 ++ journal/journal.go | 54 +++++++++++++++++++++++++++++++++++++++++ journal/journal_test.go | 21 ++++++++++++++++ pretty/pretty_test.go | 1 + wizard/wizard_test.go | 1 + 12 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 anywork/worker_test.go create mode 100644 fail/fail_test.go create mode 100644 journal/journal.go create mode 100644 journal/journal_test.go create mode 100644 pretty/pretty_test.go create mode 100644 wizard/wizard_test.go diff --git a/anywork/worker_test.go b/anywork/worker_test.go new file mode 100644 index 00000000..b748d05a --- /dev/null +++ b/anywork/worker_test.go @@ -0,0 +1 @@ +package anywork_test diff --git a/common/variables.go b/common/variables.go index 99fac04d..d556288b 100644 --- a/common/variables.go +++ b/common/variables.go @@ -61,6 +61,10 @@ func ensureDirectory(name string) string { return name } +func EventJournal() string { + return filepath.Join(RobocorpHome(), "event.log") +} + func TemplateLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "templates")) } diff --git a/common/version.go b/common/version.go index adeb30e5..5a2356bf 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.18.0` + Version = `v9.19.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 1a190bf1..f6a20b18 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v9.19.0 (date: 8.6.2021) + +- added event journaling support (no user visible yet) +- added first event "space-used" in holotree restore operations (this enables + tracking of all places where environments are created) + ## v9.18.0 (date: 3.6.2021) - now using holotree location from catalog, so that catalog decides where diff --git a/fail/fail_test.go b/fail/fail_test.go new file mode 100644 index 00000000..45b9226f --- /dev/null +++ b/fail/fail_test.go @@ -0,0 +1 @@ +package fail_test diff --git a/htfs/library.go b/htfs/library.go index 3a29ffd8..13630d3b 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -17,6 +17,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" ) @@ -264,16 +265,18 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) + catalog := it.CatalogPath(key) common.Timeline("holotree restore start %s", key) prefix := textual(sipit(client), 9) suffix := textual(sipit(tag), 8) name := prefix + "_" + suffix fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) - err = fs.LoadFrom(it.CatalogPath(key)) - fail.On(err != nil, "Failed to load catalog %s -> %v", it.CatalogPath(key), err) + err = fs.LoadFrom(catalog) + fail.On(err != nil, "Failed to load catalog %s -> %v", catalog, err) metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) + journal.Post("space-used", metafile, "normal holotree with blueprint %s from %s", key, catalog) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { diff --git a/htfs/virtual.go b/htfs/virtual.go index 8cc5c3cf..344b23d7 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -8,6 +8,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/journal" ) type virtual struct { @@ -70,6 +71,7 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { name := prefix + "_" + suffix metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(common.HolotreeLocation(), name) + journal.Post("space-used", metafile, "virutal holotree with blueprint %s", key) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 40d685d1..b9234f63 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -11,6 +11,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/journal" ) type ziplibrary struct { @@ -90,6 +91,7 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err fail.On(err != nil, "Failed to read catalog %q -> %v", catalog, err) metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) + journal.Post("space-used", metafile, "zipped holotree with blueprint %s from %s", key, catalog) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) if err == nil { diff --git a/journal/journal.go b/journal/journal.go new file mode 100644 index 00000000..70bf3b9b --- /dev/null +++ b/journal/journal.go @@ -0,0 +1,54 @@ +package journal + +import ( + "encoding/json" + "fmt" + "os" + "regexp" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" +) + +var ( + spacePattern = regexp.MustCompile("\\s+") +) + +type Event struct { + When int64 `json:"when"` + Controller string `json:"controller"` + Event string `json:"event"` + Detail string `json:"detail"` + Comment string `json:"comment,omitempty"` +} + +func Unify(value string) string { + return strings.TrimSpace(spacePattern.ReplaceAllString(value, " ")) +} + +func Post(event, detail, commentForm string, fields ...interface{}) (err error) { + defer fail.Around(&err) + message := Event{ + When: common.When, + Controller: common.ControllerIdentity(), + Event: Unify(event), + Detail: detail, + Comment: Unify(fmt.Sprintf(commentForm, fields...)), + } + blob, err := json.Marshal(message) + fail.On(err != nil, "Could not serialize event: %v -> %v", event, err) + return appendJournal(blob) +} + +func appendJournal(blob []byte) (err error) { + defer fail.Around(&err) + handle, err := os.OpenFile(common.EventJournal(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) + fail.On(err != nil, "Failed to open event journal %v -> %v", common.EventJournal(), err) + defer handle.Close() + _, err = handle.Write(blob) + fail.On(err != nil, "Failed to write event journal %v -> %v", common.EventJournal(), err) + _, err = handle.Write([]byte{'\n'}) + fail.On(err != nil, "Failed to write event journal %v -> %v", common.EventJournal(), err) + return handle.Sync() +} diff --git a/journal/journal_test.go b/journal/journal_test.go new file mode 100644 index 00000000..e393229b --- /dev/null +++ b/journal/journal_test.go @@ -0,0 +1,21 @@ +package journal_test + +import ( + "testing" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/hamlet" + "github.com/robocorp/rcc/journal" +) + +func TestJounalCanBeCalled(t *testing.T) { + must, wont := hamlet.Specifications(t) + + wont.Nil(must) + + must.Equal("foo bar", journal.Unify(" foo \t \r\n bar ")) + + common.ControllerType = "unittest" + + must.Nil(journal.Post("unittest", "journal", "from journal/journal_test.go")) +} diff --git a/pretty/pretty_test.go b/pretty/pretty_test.go new file mode 100644 index 00000000..69a3f3cf --- /dev/null +++ b/pretty/pretty_test.go @@ -0,0 +1 @@ +package pretty_test diff --git a/wizard/wizard_test.go b/wizard/wizard_test.go new file mode 100644 index 00000000..5fcd4e55 --- /dev/null +++ b/wizard/wizard_test.go @@ -0,0 +1 @@ +package wizard_test From 3bdd7797564523c3eaeeb1b425eeae80d53c6f09 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 8 Jun 2021 16:12:10 +0300 Subject: [PATCH 143/516] FIXES: locking holotree actions (v9.19.1) - added locking of holotree into new environment building and recording --- common/variables.go | 8 ++++++++ common/version.go | 2 +- conda/cleanup.go | 2 +- conda/robocorp.go | 4 ---- conda/workflows.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 11 +++++++++++ 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/common/variables.go b/common/variables.go index d556288b..d0e6cd46 100644 --- a/common/variables.go +++ b/common/variables.go @@ -48,6 +48,10 @@ func RobocorpHome() string { return ensureDirectory(ExpandPath(defaultRobocorpLocation)) } +func RobocorpLock() string { + return fmt.Sprintf("%s.lck", LiveLocation()) +} + func VerboseEnvironmentBuilding() bool { return len(os.Getenv(VERBOSE_ENVIRONMENT_BUILDING)) > 0 } @@ -93,6 +97,10 @@ func HololibLibraryLocation() string { return ensureDirectory(filepath.Join(HololibLocation(), "library")) } +func HolotreeLock() string { + return fmt.Sprintf("%s.lck", HolotreeLocation()) +} + func HolotreeLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "holotree")) } diff --git a/common/version.go b/common/version.go index 5a2356bf..5ffa3bb4 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.19.0` + Version = `v9.19.1` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index c62eea9b..15268eaf 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -128,7 +128,7 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { } func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) error { - lockfile := RobocorpLock() + lockfile := common.RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { common.Log("Could not get lock on live environment. Quitting!") diff --git a/conda/robocorp.go b/conda/robocorp.go index 9a40c97e..00f31f56 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -252,10 +252,6 @@ func RobocorpTemp() string { return fullpath } -func RobocorpLock() string { - return fmt.Sprintf("%s.lck", common.LiveLocation()) -} - func MinicondaLocation() string { // Legacy function, but must remain until cleanup is done return filepath.Join(common.RobocorpHome(), "miniconda3") diff --git a/conda/workflows.go b/conda/workflows.go index 81b3ad56..b904688a 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -341,7 +341,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { common.Timeline("New environment.") cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) - lockfile := RobocorpLock() + lockfile := common.RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { common.Log("Could not get lock on live environment. Quitting!") diff --git a/docs/changelog.md b/docs/changelog.md index f6a20b18..953d6c04 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.19.1 (date: 8.6.2021) + +- added locking of holotree into new environment building and recording + ## v9.19.0 (date: 8.6.2021) - added event journaling support (no user visible yet) diff --git a/htfs/commands.go b/htfs/commands.go index a3440e0e..3292bb3b 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/robot" ) @@ -22,6 +23,11 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) + lockfile := common.HolotreeLock() + locker, err := pathlib.Locker(lockfile, 30000) + fail.On(err != nil, "Could not get lock for holotree. Quiting.") + defer locker.Release() + haszip := len(holozip) > 0 common.Timeline("new holotree environment") @@ -57,6 +63,11 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er func RecordCondaEnvironment(tree MutableLibrary, condafile string, force bool) (err error) { defer fail.Around(&err) + lockfile := common.HolotreeLock() + locker, err := pathlib.Locker(lockfile, 30000) + fail.On(err != nil, "Could not get lock for holotree. Quiting.") + defer locker.Release() + right, err := conda.ReadCondaYaml(condafile) fail.On(err != nil, "Could not load environmet config %q, reason: %w", condafile, err) From 73c413637f878f0abfcb80a8bccfb1a0503c5396 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 9 Jun 2021 14:37:45 +0300 Subject: [PATCH 144/516] FIXES: locking holotree actions (v9.19.2) - added locking of holotree into environment restoring --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 6 ++---- htfs/library.go | 4 ++++ htfs/virtual.go | 5 +++++ htfs/ziplibrary.go | 5 +++++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 5ffa3bb4..2ca3a3f2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.19.1` + Version = `v9.19.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 953d6c04..a1e36f55 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.19.2 (date: 9.6.2021) + +- added locking of holotree into environment restoring + ## v9.19.1 (date: 8.6.2021) - added locking of holotree into new environment building and recording diff --git a/htfs/commands.go b/htfs/commands.go index 3292bb3b..e81425cf 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -23,8 +23,7 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) - lockfile := common.HolotreeLock() - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(common.HolotreeLock(), 30000) fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() @@ -63,8 +62,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er func RecordCondaEnvironment(tree MutableLibrary, condafile string, force bool) (err error) { defer fail.Around(&err) - lockfile := common.HolotreeLock() - locker, err := pathlib.Locker(lockfile, 30000) + locker, err := pathlib.Locker(common.HolotreeLock(), 30000) fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/htfs/library.go b/htfs/library.go index 13630d3b..8f952795 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -276,6 +276,10 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er fail.On(err != nil, "Failed to load catalog %s -> %v", catalog, err) metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) + lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + locker, err := pathlib.Locker(lockfile, 30000) + fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) + defer locker.Release() journal.Post("space-used", metafile, "normal holotree with blueprint %s from %s", key, catalog) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) diff --git a/htfs/virtual.go b/htfs/virtual.go index 344b23d7..99f786ca 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -9,6 +9,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" + "github.com/robocorp/rcc/pathlib" ) type virtual struct { @@ -71,6 +72,10 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { name := prefix + "_" + suffix metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(common.HolotreeLocation(), name) + lockfile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) + locker, err := pathlib.Locker(lockfile, 30000) + fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) + defer locker.Release() journal.Post("space-used", metafile, "virutal holotree with blueprint %s", key) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index b9234f63..66d2bcae 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -12,6 +12,7 @@ import ( "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" + "github.com/robocorp/rcc/pathlib" ) type ziplibrary struct { @@ -91,6 +92,10 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err fail.On(err != nil, "Failed to read catalog %q -> %v", catalog, err) metafile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(fs.HolotreeBase(), name) + lockfile := filepath.Join(fs.HolotreeBase(), fmt.Sprintf("%s.lck", name)) + locker, err := pathlib.Locker(lockfile, 30000) + fail.On(err != nil, "Could not get lock for %s. Quiting.", targetdir) + defer locker.Release() journal.Post("space-used", metafile, "zipped holotree with blueprint %s from %s", key, catalog) currentstate := make(map[string]string) shadow, err := NewRoot(targetdir) From c2cd57b31e20025bd7b731c2b2ef3d2241e2563f Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Jun 2021 08:24:53 +0300 Subject: [PATCH 145/516] RCC-181: event journaling (v9.19.3) - added support for getting list of events out - fix: moved holotree changes messages to trace level --- cmd/events.go | 45 +++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/functions.go | 8 ++++---- journal/journal.go | 25 +++++++++++++++++++++++ journal/journal_test.go | 11 +++++++--- 6 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 cmd/events.go diff --git a/cmd/events.go b/cmd/events.go new file mode 100644 index 00000000..c3b95e39 --- /dev/null +++ b/cmd/events.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + "github.com/robocorp/rcc/journal" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +func humaneEventListing(events []journal.Event) { + tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) + tabbed.Write([]byte("When\tController\tEvent\tDetail\tComment\n")) + tabbed.Write([]byte("----\t----------\t-----\t------\t-------\n")) + for _, event := range events { + data := fmt.Sprintf("%d\t%s\t%s\t%s\t%s\n", event.When, event.Controller, event.Event, event.Detail, event.Comment) + tabbed.Write([]byte(data)) + } + tabbed.Flush() +} + +var eventsCmd = &cobra.Command{ + Use: "events", + Short: "Show events from event journal (ROBOCORP_HOME/event.log).", + Long: "Show events from event journal (ROBOCORP_HOME/event.log).", + Run: func(cmd *cobra.Command, args []string) { + events, err := journal.Events() + pretty.Guard(err == nil, 2, "Error while loading events: %v", err) + if jsonFlag { + output, err := json.MarshalIndent(events, "", " ") + pretty.Guard(err == nil, 3, "Error while converting events: %v", err) + fmt.Fprintln(os.Stdout, string(output)) + } else { + humaneEventListing(events) + } + }, +} + +func init() { + configureCmd.AddCommand(eventsCmd) + eventsCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Show effective settings as JSON stream.") +} diff --git a/common/version.go b/common/version.go index 2ca3a3f2..1d93ecc8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.19.2` + Version = `v9.19.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index a1e36f55..a6e059c0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v9.19.3 (date: 9.6.2021) + +- added support for getting list of events out +- fix: moved holotree changes messages to trace level + ## v9.19.2 (date: 9.6.2021) - added locking of holotree into environment restoring diff --git a/htfs/functions.go b/htfs/functions.go index d2fbbe31..0da33e84 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -274,7 +274,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat if info.IsDir() { _, ok := it.Dirs[part.Name()] if !ok { - common.Debug("* Holotree: remove extra directory %q", directpath) + common.Trace("* Holotree: remove extra directory %q", directpath) anywork.Backlog(RemoveDirectory(directpath)) } stats.Dirty(!ok) @@ -283,7 +283,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat files[part.Name()] = true found, ok := it.Files[part.Name()] if !ok { - common.Debug("* Holotree: remove extra file %q", directpath) + common.Trace("* Holotree: remove extra file %q", directpath) anywork.Backlog(RemoveFile(directpath)) stats.Dirty(true) continue @@ -293,7 +293,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat ok = golden && found.Match(info) stats.Dirty(!ok) if !ok { - common.Debug("* Holotree: update changed file %q", directpath) + common.Trace("* Holotree: update changed file %q", directpath) anywork.Backlog(DropFile(library, found.Digest, directpath, found, fs.Rewrite())) } } @@ -302,7 +302,7 @@ func RestoreDirectory(library Library, fs *Root, current map[string]string, stat _, seen := files[name] if !seen { stats.Dirty(true) - common.Debug("* Holotree: add missing file %q", directpath) + common.Trace("* Holotree: add missing file %q", directpath) anywork.Backlog(DropFile(library, found.Digest, directpath, found, fs.Rewrite())) } } diff --git a/journal/journal.go b/journal/journal.go index 70bf3b9b..c4c01bc0 100644 --- a/journal/journal.go +++ b/journal/journal.go @@ -1,8 +1,10 @@ package journal import ( + "bufio" "encoding/json" "fmt" + "io" "os" "regexp" "strings" @@ -52,3 +54,26 @@ func appendJournal(blob []byte) (err error) { fail.On(err != nil, "Failed to write event journal %v -> %v", common.EventJournal(), err) return handle.Sync() } + +func Events() (result []Event, err error) { + defer fail.Around(&err) + handle, err := os.Open(common.EventJournal()) + fail.On(err != nil, "Failed to open event journal %v -> %v", common.EventJournal(), err) + defer handle.Close() + source := bufio.NewReader(handle) + fail.On(err != nil, "Failed to read %s.", common.EventJournal()) + result = make([]Event, 0, 100) + for { + line, err := source.ReadBytes('\n') + if err == io.EOF { + return result, nil + } + fail.On(err != nil, "Failed to read %s.", common.EventJournal()) + event := Event{} + err = json.Unmarshal(line, &event) + if err != nil { + continue + } + result = append(result, event) + } +} diff --git a/journal/journal_test.go b/journal/journal_test.go index e393229b..9292e002 100644 --- a/journal/journal_test.go +++ b/journal/journal_test.go @@ -11,11 +11,16 @@ import ( func TestJounalCanBeCalled(t *testing.T) { must, wont := hamlet.Specifications(t) - wont.Nil(must) - must.Equal("foo bar", journal.Unify(" foo \t \r\n bar ")) common.ControllerType = "unittest" - must.Nil(journal.Post("unittest", "journal", "from journal/journal_test.go")) + must.Nil(journal.Post("unittest", "journal-1", "from journal/journal_test.go")) + events, err := journal.Events() + must.Nil(err) + wont.Nil(events) + must.True(len(events) > 0) + must.Nil(journal.Post("unittest", "journal-2", "from journal/journal_test.go")) + second, err := journal.Events() + must.True(len(second) > len(events)) } From dca0d2a2be99f5472947c604b8cbc41e95464097 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Jun 2021 13:08:18 +0300 Subject: [PATCH 146/516] FIXES: holotree functionality (v9.19.4) - added json format to `rcc holotree export` output formats - added docs/recipes.md and also new command `rcc docs recipes` - added links to README.md to internal documentation --- README.md | 4 ++++ Rakefile | 2 +- cmd/holotreeExport.go | 18 +++++++++++++----- cmd/recipes.go | 27 +++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 8 +++++++- docs/recipes.md | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 cmd/recipes.go create mode 100644 docs/recipes.md diff --git a/README.md b/README.md index af8cd490..fede29ea 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Follow above link to download site. Both tested and bleeding edge versions are a Visit [https://robocorp.com/docs](https://robocorp.com/docs) to view the full documentation on the full Robocorp stack. +Changelog can be seen [here.](/docs/changelog.md) It is also visible inside rcc using command `rcc docs changelog`. + +Some tips, tricks, and recipes can be found [here.](/docs/recipes.md) They are also visible inside rcc using command `rcc docs recipes`. + ## Community The Robocorp community can be found on [Developer Slack](https://robocorp-developers.slack.com), where you can ask questions, voice ideas, and share your projects. diff --git a/Rakefile b/Rakefile index 11236219..7e6788f9 100644 --- a/Rakefile +++ b/Rakefile @@ -30,7 +30,7 @@ task :assets do puts "Directory #{directory} => #{assetname}" sh "cd #{directory} && zip -ryqD9 #{assetname} ." end - sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md" + sh "$HOME/go/bin/go-bindata -o blobs/assets.go -pkg blobs assets/*.yaml assets/*.zip assets/man/* docs/changelog.md docs/recipes.md" end task :clean do diff --git a/cmd/holotreeExport.go b/cmd/holotreeExport.go index a2d02739..c4c2c09f 100644 --- a/cmd/holotreeExport.go +++ b/cmd/holotreeExport.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "strings" "github.com/robocorp/rcc/common" @@ -26,10 +27,16 @@ func holotreeExport(catalogs []string, archive string) { pretty.Guard(err == nil, 3, "%s", err) } -func listCatalogs() { - common.Log("Selectable catalogs (you can use substrings):") - for _, catalog := range htfs.Catalogs() { - common.Log("- %s", catalog) +func listCatalogs(jsonForm bool) { + if jsonForm { + nice, err := json.MarshalIndent(htfs.Catalogs(), "", " ") + pretty.Guard(err == nil, 2, "%s", err) + common.Stdout("%s\n", nice) + } else { + common.Log("Selectable catalogs (you can use substrings):") + for _, catalog := range htfs.Catalogs() { + common.Log("- %s", catalog) + } } } @@ -55,7 +62,7 @@ var holotreeExportCmd = &cobra.Command{ defer common.Stopwatch("Holotree export command lasted").Report() } if len(args) == 0 { - listCatalogs() + listCatalogs(jsonFlag) } else { holotreeExport(selectCatalogs(args), holozip) } @@ -66,4 +73,5 @@ var holotreeExportCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeExportCmd) holotreeExportCmd.Flags().StringVarP(&holozip, "zipfile", "z", "hololib.zip", "Name of zipfile to export.") + holotreeExportCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") } diff --git a/cmd/recipes.go b/cmd/recipes.go new file mode 100644 index 00000000..cf1ee998 --- /dev/null +++ b/cmd/recipes.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/robocorp/rcc/blobs" + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var recipesCmd = &cobra.Command{ + Use: "recipes", + Short: "Show rcc recipes, tips, and tricks.", + Long: "Show rcc recipes, tips, and tricks.", + Aliases: []string{"recipe", "tips", "tricks"}, + Run: func(cmd *cobra.Command, args []string) { + content, err := blobs.Asset("docs/recipes.md") + if err != nil { + pretty.Exit(1, "Cannot show recipes.md, reason: %v", err) + } + common.Stdout("\n%s\n", content) + }, +} + +func init() { + manCmd.AddCommand(recipesCmd) +} diff --git a/common/version.go b/common/version.go index 1d93ecc8..22c150a5 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.19.3` + Version = `v9.19.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index a6e059c0..dd65f946 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,12 @@ # rcc change log -## v9.19.3 (date: 9.6.2021) +## v9.19.4 (date: 10.6.2021) + +- added json format to `rcc holotree export` output formats +- added docs/recipes.md and also new command `rcc docs recipes` +- added links to README.md to internal documentation + +## v9.19.3 (date: 10.6.2021) - added support for getting list of events out - fix: moved holotree changes messages to trace level diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 00000000..72cdf992 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,33 @@ +# Tips, tricks, and recipies + +## How pass arguments to robot from CLI + +Since version 9.15.0, rcc supports passing arguments from CLI to underlying +robot. For that, you need to have task in `robot.yaml` that co-operates with +additional arguments appended at the end of given `shell` command. + +### Example robot.yaml with scripting task + +```yaml +tasks: + Run all tasks: + shell: python -m robot --report NONE --outputdir output --logtitle "Task log" tasks.robot + + scripting: + shell: python -m robot --report NONE --outputdir output --logtitle "Scripting log" + +condaConfigFile: conda.yaml +artifactsDir: output +PATH: + - . +PYTHONPATH: + - . +ignoreFiles: + - .gitignore +``` + +### Run it with `--` separator. + +```sh +rcc task run --interactive --task scripting -- --loglevel TRACE --variable answer:42 tasks.robot +``` From 9b0193dd5758c2df078207546143a1c4175514ae Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 10 Jun 2021 15:29:36 +0300 Subject: [PATCH 147/516] RCC-165: run anything inside robot space (v9.20.0) - added `rcc task script` command for running anything inside robot environment --- cmd/script.go | 48 +++++++++++++++++++++++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 ++++ docs/recipes.md | 23 ++++++++++++++++++++- operations/running.go | 3 ++- 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 cmd/script.go diff --git a/cmd/script.go b/cmd/script.go new file mode 100644 index 00000000..33484fb1 --- /dev/null +++ b/cmd/script.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/operations" + + "github.com/spf13/cobra" +) + +var scriptCmd = &cobra.Command{ + Use: "script", + Short: "Run script inside robot task envrionment.", + Long: "Run script inside robot task envrionment.", + Example: ` + rcc task script -- pip list + rcc task script --silent -- python --version +`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Task run lasted").Report() + } + simple, config, todo, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) + operations.SelectExecutionModel(noRunFlags(), simple, args, config, todo, label, interactiveFlag, nil) + }, +} + +func noRunFlags() *operations.RunFlags { + return &operations.RunFlags{ + AccountName: "", + WorkspaceId: "", + ValidityTime: 0, + EnvironmentFile: environmentFile, + RobotYaml: robotFile, + Assistant: false, + NoPipFreeze: true, + } +} + +func init() { + taskCmd.AddCommand(scriptCmd) + + scriptCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to the 'env.json' development environment data file.") + scriptCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") + scriptCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") + scriptCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") + scriptCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") +} diff --git a/common/version.go b/common/version.go index 22c150a5..b28aa3f8 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.19.4` + Version = `v9.20.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index dd65f946..7154c230 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v9.20.0 (date: 10.6.2021) + +- added `rcc task script` command for running anything inside robot environment + ## v9.19.4 (date: 10.6.2021) - added json format to `rcc holotree export` output formats diff --git a/docs/recipes.md b/docs/recipes.md index 72cdf992..66ad2b4d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -1,6 +1,6 @@ # Tips, tricks, and recipies -## How pass arguments to robot from CLI +## How pass arguments to robot from CLI? Since version 9.15.0, rcc supports passing arguments from CLI to underlying robot. For that, you need to have task in `robot.yaml` that co-operates with @@ -31,3 +31,24 @@ ignoreFiles: ```sh rcc task run --interactive --task scripting -- --loglevel TRACE --variable answer:42 tasks.robot ``` + +## How to run any command inside robot environment? + +Since version 9.20.0, rcc now supports running any command inside robot space +using `rcc task script` command. + +### Some example commands + +Run following commands in same direcotry where your `robot.yaml` is. Or +otherwise you have to provide `--robot path/to/robot.yaml` in commandline. + +```sh +# what python version we are running +rcc task script --silent -- python --version + +# get pip list from this environment +rcc task script --silent -- pip list + +# start interactive ipython session +rcc task script --interactive -- ipython +``` diff --git a/operations/running.go b/operations/running.go index 5ad2571e..86ab72a5 100644 --- a/operations/running.go +++ b/operations/running.go @@ -26,6 +26,7 @@ type RunFlags struct { EnvironmentFile string RobotYaml string Assistant bool + NoPipFreeze bool } func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { @@ -212,7 +213,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro before := make(map[string]string) beforeHash, beforeErr := conda.DigestFor(label, before) outputDir := config.ArtifactDirectory() - if !flags.Assistant && !common.Silent && !interactive { + if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } common.Debug("DEBUG: about to run command - %v", task) From 0ec1c13c181ff218951c8c18fbf300d60d1b0f73 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 15 Jun 2021 11:55:02 +0300 Subject: [PATCH 148/516] RCC-182: remove lease support (v10.0.0) - removed lease support, this is major breaking change (if someone was using it) --- cmd/env.go | 1 - cmd/envUnlease.go | 37 -------- cmd/list.go | 6 +- cmd/rcc/main.go | 2 +- common/variables.go | 6 -- common/version.go | 2 +- conda/cleanup.go | 17 ---- conda/lease.go | 146 -------------------------------- conda/workflows.go | 11 --- docs/changelog.md | 4 + robot_tests/fullrun.robot | 2 +- robot_tests/lease.robot | 87 ------------------- robot_tests/leasebot/.gitignore | 13 --- robot_tests/leasebot/conda.yaml | 5 -- robot_tests/leasebot/robot.yaml | 12 --- 15 files changed, 9 insertions(+), 342 deletions(-) delete mode 100644 cmd/envUnlease.go delete mode 100644 conda/lease.go delete mode 100644 robot_tests/lease.robot delete mode 100755 robot_tests/leasebot/.gitignore delete mode 100755 robot_tests/leasebot/conda.yaml delete mode 100755 robot_tests/leasebot/robot.yaml diff --git a/cmd/env.go b/cmd/env.go index 3b77f151..5be410f8 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -20,6 +20,5 @@ used in task context locally.`, func init() { rootCmd.AddCommand(envCmd) - envCmd.PersistentFlags().StringVar(&common.LeaseContract, "lease", "", "unique lease contract for long living environments") envCmd.PersistentFlags().StringVar(&common.StageFolder, "stage", "", "internal, DO NOT USE (unless you know what you are doing)") } diff --git a/cmd/envUnlease.go b/cmd/envUnlease.go deleted file mode 100644 index 43662f50..00000000 --- a/cmd/envUnlease.go +++ /dev/null @@ -1,37 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var ( - leaseHash string -) - -var envUnleaseCmd = &cobra.Command{ - Use: "unlease", - Short: "Drop existing lease of given environment.", - Long: "Drop existing lease of given environment.", - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Conda env unlease lasted").Report() - } - err := conda.DropLease(leaseHash, common.LeaseContract) - if err != nil { - pretty.Exit(1, "Error: %v", err) - } - pretty.Ok() - }, -} - -func init() { - envCmd.AddCommand(envUnleaseCmd) - envUnleaseCmd.Flags().StringVar(&common.LeaseContract, "lease", "", "unique lease contract for long living environments") - envUnleaseCmd.MarkFlagRequired("lease") - envUnleaseCmd.Flags().StringVar(&leaseHash, "hash", "", "hash identity of leased environment") - envUnleaseCmd.MarkFlagRequired("hash") -} diff --git a/cmd/list.go b/cmd/list.go index f2cf1955..56e3eb2b 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -41,7 +41,7 @@ in human readable form.`, lines := make([]string, 0, len(templates)) entries := make(map[string]interface{}) if !jsonFlag { - common.Log("%-25s %-25s %-16s %5s %s", "Last used", "Last cloned", "Environment", "Plan?", "Leased duration") + common.Log("%-25s %-25s %-16s %5s", "Last used", "Last cloned", "Environment", "Plan?") } for _, template := range templates { details := make(map[string]interface{}) @@ -59,12 +59,10 @@ in human readable form.`, details["name"] = template details["used"] = used details["cloned"] = cloned - details["leased"] = conda.WhoLeased(template) - details["expires"] = conda.LeaseExpires(template) details["base"] = conda.TemplateFrom(template) details["live"] = conda.LiveFrom(template) planfile, plan := conda.InstallationPlan(template) - lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %5v %q %s", used, cloned, template, plan, conda.WhoLeased(template), conda.LeaseExpires(template))) + lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %5v", used, cloned, template, plan)) if plan { details["plan"] = planfile } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index d870a1d5..30e09cc6 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -54,7 +54,7 @@ func startTempRecycling() { } func markTempForRecycling() { - if common.LeaseEffective || markedAlready { + if markedAlready { return } markedAlready = true diff --git a/common/variables.go b/common/variables.go index d0e6cd46..f648539a 100644 --- a/common/variables.go +++ b/common/variables.go @@ -24,11 +24,9 @@ var ( NoOutputCapture bool Liveonly bool Stageonly bool - LeaseEffective bool StageFolder string ControllerType string HolotreeSpace string - LeaseContract string EnvironmentHash string SemanticTag string When int64 @@ -151,7 +149,3 @@ func UserAgent() string { func ControllerIdentity() string { return strings.ToLower(fmt.Sprintf("rcc.%s", ControllerType)) } - -func IsLeaseRequest() bool { - return len(LeaseContract) > 0 -} diff --git a/common/version.go b/common/version.go index b28aa3f8..2435bd74 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v9.20.0` + Version = `v10.0.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 15268eaf..5eed9ac4 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -1,7 +1,6 @@ package conda import ( - "fmt" "os" "path/filepath" "time" @@ -59,9 +58,6 @@ func orphanCleanup(dryrun bool) error { } func spotlessCleanup(dryrun bool) error { - if anyLeasedEnvironment() { - return fmt.Errorf("Cannot clean everything, since there are some leased environments!") - } if dryrun { common.Log("Would be removing:") common.Log("- %v", common.BaseLocation()) @@ -85,15 +81,6 @@ func spotlessCleanup(dryrun bool) error { return nil } -func anyLeasedEnvironment() bool { - for _, template := range TemplateList() { - if IsLeasedEnvironment(template) { - return true - } - } - return false -} - func cleanupTemp(deadline time.Time, dryrun bool) error { basedir := RobocorpTempRoot() handle, err := os.Open(basedir) @@ -157,10 +144,6 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) err if !all && whenBase.After(deadline) { continue } - if IsLeasedEnvironment(template) { - common.Log("WARNING: %q is leased by %q and wont be cleaned up!", template, WhoLeased(template)) - continue - } if dryrun { common.Log("Would be removing %v.", template) continue diff --git a/conda/lease.go b/conda/lease.go deleted file mode 100644 index 2f1fb2ab..00000000 --- a/conda/lease.go +++ /dev/null @@ -1,146 +0,0 @@ -package conda - -import ( - "fmt" - "io/ioutil" - "os" - "strings" - "time" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/pathlib" -) - -func LeaseInterceptor(hash string) (string, bool, error) { - if !common.IsLeaseRequest() { - err := DoesLeasingAllowUsage(hash) - if err != nil { - return "", false, err - } - } else { - leased := IsLeasedEnvironment(hash) - if leased && CouldExtendLease(hash) { - common.Debug("Lease of %q was extended for %q!", hash, WhoLeased(hash)) - return LiveFrom(hash), true, nil - } - if leased && !IsLeasePristine(hash) { - return "", false, fmt.Errorf("Cannot get environment %q because it is dirty and leased by %q!", hash, WhoLeased(hash)) - } - if !IsLeasedEnvironment(hash) { - err := TakeLease(hash, common.LeaseContract) - if err != nil { - return "", false, err - } - common.Debug("Lease of %q taken by %q!", hash, WhoLeased(hash)) - } - } - return "", false, nil -} - -func DoesLeasingAllowUsage(hash string) error { - if !IsLeasedEnvironment(hash) || IsLeasePristine(hash) { - return nil - } - reason, err := readLeaseFile(hash) - if err != nil { - return err - } - return fmt.Errorf("Environment leased to %q is dirty! Cannot use it for now!", reason) -} - -func CouldExtendLease(hash string) bool { - reason, err := readLeaseFile(hash) - if err != nil { - return false - } - if reason != common.LeaseContract { - return false - } - pathlib.TouchWhen(LeaseFileFrom(hash), time.Now()) - common.LeaseEffective = true - return true -} - -func TakeLease(hash, reason string) error { - err := writeLeaseFile(hash, reason) - if err != nil { - return err - } - common.LeaseEffective = true - return nil -} - -func DropLease(hash, reason string) error { - if !IsLeasedEnvironment(hash) { - return fmt.Errorf("Not a leased environment: %q!", hash) - } - if reason != WhoLeased(hash) { - return fmt.Errorf("Environment %q is not leased by %q!", hash, reason) - } - return os.Remove(LeaseFileFrom(hash)) -} - -func WhoLeased(hash string) string { - if !IsLeasedEnvironment(hash) { - return "~" - } - reason, err := readLeaseFile(hash) - if err != nil { - return err.Error() - } - return reason -} - -func LeaseExpires(hash string) time.Duration { - leasefile := LeaseFileFrom(hash) - stamp, err := pathlib.Modtime(leasefile) - if err != nil { - return 0 * time.Second - } - deadline := stamp.Add(1 * time.Hour) - delta := deadline.Sub(time.Now()).Round(1 * time.Second) - if delta < 0*time.Second { - return 0 * time.Second - } - return delta -} - -func IsLeasedEnvironment(hash string) bool { - leasefile := LeaseFileFrom(hash) - exists := pathlib.IsFile(leasefile) - if !exists { - return false - } - return LeaseExpires(hash) > 0*time.Second -} - -func IsLeasePristine(hash string) bool { - return IsPristine(LiveFrom(hash)) -} - -func LeaseFileFrom(hash string) string { - return fmt.Sprintf("%s.lease", LiveFrom(hash)) -} - -func IsSameLease(hash, reason string) bool { - if !IsLeasedEnvironment(hash) { - return false - } - content, err := readLeaseFile(hash) - return err == nil && content == reason -} - -func readLeaseFile(hash string) (string, error) { - leasefile := LeaseFileFrom(hash) - content, err := ioutil.ReadFile(leasefile) - if err != nil { - return "", err - } - return string(content), nil -} - -func writeLeaseFile(hash, reason string) error { - leasefile := LeaseFileFrom(hash) - flatReason := strings.TrimSpace(reason) - return ioutil.WriteFile(leasefile, []byte(flatReason), 0o640) -} diff --git a/conda/workflows.go b/conda/workflows.go index b904688a..853a9267 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -389,14 +389,6 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { common.EnvironmentHash = key - quickFolder, ok, err := LeaseInterceptor(key) - if ok { - return quickFolder, nil - } - if err != nil { - return "", err - } - liveFolder := LiveFrom(key) after := make(map[string]string) afterHash, afterErr := DigestFor(liveFolder, after) @@ -472,9 +464,6 @@ func FindEnvironment(prefix string) []string { } func RemoveEnvironment(label string) error { - if IsLeasedEnvironment(label) { - return fmt.Errorf("WARNING: %q is leased by %q and wont be deleted!", label, WhoLeased(label)) - } err := removeClone(LiveFrom(label)) if err != nil { return err diff --git a/docs/changelog.md b/docs/changelog.md index 7154c230..338ef099 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.0.0 (date: 15.6.2021) + +- removed lease support, this is major breaking change (if someone was using it) + ## v9.20.0 (date: 10.6.2021) - added `rcc task script` command for running anything inside robot environment diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 6b42eb6d..1a4a3201 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -9,7 +9,7 @@ Using and running template example with shell file Goal Show rcc version information. Step build/rcc version --controller citests - Must Have v9. + Must Have v10. Goal Show rcc license information. Step build/rcc man license --controller citests diff --git a/robot_tests/lease.robot b/robot_tests/lease.robot deleted file mode 100644 index 23206367..00000000 --- a/robot_tests/lease.robot +++ /dev/null @@ -1,87 +0,0 @@ -*** Settings *** -Resource resources.robot - -*** Test cases *** -Can operate leased environments - - Goal Create environment with lease - Step build/rcc env variables --lease "taker (1)" robot_tests/leasebot/conda.yaml - Must Have CONDA_DEFAULT_ENV=rcc - Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - - Goal Check listing for taker information - Step build/rcc env list - Use STDERR - Must Have "taker (1)" - - Goal Others can get same environment - Step build/rcc env variables robot_tests/leasebot/conda.yaml - Must Have CONDA_DEFAULT_ENV=rcc - Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - - Goal Can share environment, but wont own the lease - Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml - Must Have CONDA_DEFAULT_ENV=rcc - Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - - Goal Check listing for taker information (still same) - Step build/rcc env list - Use STDERR - Must Have "taker (1)" - Wont Have "second (2)" - - Goal Lets corrupt the environment - Step build/rcc task run -r robot_tests/leasebot/robot.yaml - Must Have Successfully installed pytz - - Goal Now others cannot get same environment anymore - Step build/rcc env variables robot_tests/leasebot/conda.yaml 1 - Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - Wont Have CONDA_DEFAULT_ENV=rcc - Use STDERR - Must Have Environment leased to "taker (1)" is dirty - - Goal Cannot share environment, since it is dirty - Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml 1 - Wont Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - Wont Have CONDA_DEFAULT_ENV=rcc - Use STDERR - Must Have Cannot get environment "8f1d3dc95228edef" because it is dirty and leased by "taker (1)" - - Goal Cannot unlease someone elses environment - Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef 1 - Use STDERR - Must Have Error: - - Goal Cannot delete someone elses leased environment - Step build/rcc env delete 8f1d3dc95228edef 1 - Use STDERR - Must Have WARNING: "8f1d3dc95228edef" is leased by "taker (1)" and wont be deleted! - - Goal Check listing for taker information (still same) - Step build/rcc env list - Use STDERR - Must Have 8f1d3dc95228edef - Must Have "taker (1)" - Wont Have "second (2)" - - Goal Lease can be unleased - Step build/rcc env unlease --lease "taker (1)" --hash 8f1d3dc95228edef - Use STDERR - Must Have OK. - - Goal Others can now lease that environment - Step build/rcc env variables --lease "second (2)" robot_tests/leasebot/conda.yaml - Must Have CONDA_DEFAULT_ENV=rcc - Must Have RCC_ENVIRONMENT_HASH=8f1d3dc95228edef - - Goal Check listing for taker information (still same) - Step build/rcc env list - Use STDERR - Must Have "second (2)" - Wont Have "taker (1)" - - Goal Lease can be unleased - Step build/rcc env unlease --lease "second (2)" --hash 8f1d3dc95228edef - Use STDERR - Must Have OK. diff --git a/robot_tests/leasebot/.gitignore b/robot_tests/leasebot/.gitignore deleted file mode 100755 index 09bd393b..00000000 --- a/robot_tests/leasebot/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -testrun/ -output/ -venv/ -temp/ -.rpa/ -.idea/ -.ipynb_checkpoints/ -.virtual_documents/ -*/.ipynb_checkpoints/* -.vscode -.DS_Store -*.pyc -*.zip diff --git a/robot_tests/leasebot/conda.yaml b/robot_tests/leasebot/conda.yaml deleted file mode 100755 index d1cae0c8..00000000 --- a/robot_tests/leasebot/conda.yaml +++ /dev/null @@ -1,5 +0,0 @@ -channels: -- conda-forge -dependencies: -- python=3.8.6 -- pip=20.1 diff --git a/robot_tests/leasebot/robot.yaml b/robot_tests/leasebot/robot.yaml deleted file mode 100755 index 10b1f959..00000000 --- a/robot_tests/leasebot/robot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -tasks: - corrupt: - shell: pip install pytz - -condaConfigFile: conda.yaml -artifactsDir: output -PATH: - - . -PYTHONPATH: - - . -ignoreFiles: - - .gitignore From 0120f6a824deea37ea2d3563493f92b4db32f0a2 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 17 Jun 2021 11:59:52 +0300 Subject: [PATCH 149/516] RCC-186: limited pager support (v10.1.0) - adding pager for `rcc man xxx` documents - more trace printing on workflow setup - added [D] and [T] markers for debug and trace level log entries - when debug and trace log level is on, normal log entries are prefixed with [N] - fixed rights problem in file `rcc_plan.log` --- assets/man/tutorial.txt | 3 +- cloud/metrics.go | 8 +++--- cmd/changelog.go | 3 +- cmd/holotreeVariables.go | 2 +- cmd/license.go | 3 +- cmd/rcc/main.go | 2 +- cmd/recipes.go | 3 +- cmd/root.go | 1 + cmd/tutorial.go | 3 +- common/logger.go | 10 +++++-- common/version.go | 2 +- conda/workflows.go | 32 +++++++++++++++++++-- docs/changelog.md | 8 ++++++ docs/recipes.md | 20 +++++++++++++ go.mod | 3 +- go.sum | 5 ++++ htfs/commands.go | 2 +- operations/diagnostics.go | 2 +- operations/running.go | 4 +-- pretty/pager.go | 59 +++++++++++++++++++++++++++++++++++++++ pretty/variables.go | 12 ++++++++ 21 files changed, 159 insertions(+), 28 deletions(-) create mode 100644 pretty/pager.go diff --git a/assets/man/tutorial.txt b/assets/man/tutorial.txt index 4185d926..aee8c217 100644 --- a/assets/man/tutorial.txt +++ b/assets/man/tutorial.txt @@ -1,5 +1,4 @@ -Welcome to RCC tutorial -======================= +# Welcome to RCC tutorial Create you first Robot Framework or python robot and follow given instructions to run it: diff --git a/cloud/metrics.go b/cloud/metrics.go index 2b693aaf..b1bd0b80 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -35,7 +35,7 @@ func sendMetric(metricsHost, kind, name, value string) { } timestamp := time.Now().UnixNano() url := fmt.Sprintf(trackingUrl, url.PathEscape(kind), timestamp, url.PathEscape(xviper.TrackingIdentity()), url.PathEscape(name), url.PathEscape(value)) - common.Debug("DEBUG: Sending metric as %v%v", metricsHost, url) + common.Debug("Sending metric as %v%v", metricsHost, url) client.Put(client.NewRequest(url)) } @@ -44,7 +44,7 @@ func BackgroundMetric(kind, name, value string) { if len(metricsHost) == 0 { return } - common.Debug("DEBUG: BackgroundMetric kind:%v name:%v value:%v send:%v", kind, name, value, xviper.CanTrack()) + common.Debug("BackgroundMetric kind:%v name:%v value:%v send:%v", kind, name, value, xviper.CanTrack()) if xviper.CanTrack() { telemetryBarrier.Add(1) go sendMetric(metricsHost, kind, name, value) @@ -52,7 +52,7 @@ func BackgroundMetric(kind, name, value string) { } func WaitTelemetry() { - common.Debug("DEBUG: wait telemetry to complete") + common.Debug("wait telemetry to complete") telemetryBarrier.Wait() - common.Debug("DEBUG: telemetry sending completed") + common.Debug("telemetry sending completed") } diff --git a/cmd/changelog.go b/cmd/changelog.go index 6eb1d23d..51131d61 100644 --- a/cmd/changelog.go +++ b/cmd/changelog.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -18,7 +17,7 @@ var changelogCmd = &cobra.Command{ if err != nil { pretty.Exit(1, "Cannot show changelog.md, reason: %v", err) } - common.Stdout("\n%s\n", content) + pretty.Page(content) }, } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index a9ea8546..5795a6c2 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -28,7 +28,7 @@ func holotreeExpandEnvironment(userFiles []string, packfile, environment, worksp pretty.Guard(err == nil, 5, "%s", err) condafile := filepath.Join(conda.RobocorpTemp(), htfs.BlueprintHash(holotreeBlueprint)) - err = os.WriteFile(condafile, holotreeBlueprint, 0o640) + err = os.WriteFile(condafile, holotreeBlueprint, 0o644) pretty.Guard(err == nil, 6, "%s", err) holozip := "" diff --git a/cmd/license.go b/cmd/license.go index c6c959e9..6dc29518 100644 --- a/cmd/license.go +++ b/cmd/license.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -17,7 +16,7 @@ var licenseCmd = &cobra.Command{ if err != nil { pretty.Exit(1, "Cannot show LICENSE, reason: %v", err) } - common.Stdout("%s\n", content) + pretty.Page(content) }, } diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 30e09cc6..e10fb644 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -59,7 +59,7 @@ func markTempForRecycling() { } markedAlready = true filename := filepath.Join(conda.RobocorpTemp(), "recycle.now") - ioutil.WriteFile(filename, []byte("True"), 0o640) + ioutil.WriteFile(filename, []byte("True"), 0o644) common.Debug("Marked %q for recyling.", conda.RobocorpTemp()) } diff --git a/cmd/recipes.go b/cmd/recipes.go index cf1ee998..9cf7522a 100644 --- a/cmd/recipes.go +++ b/cmd/recipes.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -18,7 +17,7 @@ var recipesCmd = &cobra.Command{ if err != nil { pretty.Exit(1, "Cannot show recipes.md, reason: %v", err) } - common.Stdout("\n%s\n", content) + pretty.Page(content) }, } diff --git a/cmd/root.go b/cmd/root.go index 763b93e7..1362bfae 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -82,6 +82,7 @@ func Execute() { } }() + rootCmd.SetArgs(os.Args[1:]) err := rootCmd.Execute() pretty.Guard(err == nil, 1, "Error: [rcc %v] %v", common.Version, err) } diff --git a/cmd/tutorial.go b/cmd/tutorial.go index 40371f4d..9e34db24 100644 --- a/cmd/tutorial.go +++ b/cmd/tutorial.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/robocorp/rcc/blobs" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" @@ -18,7 +17,7 @@ var tutorialCmd = &cobra.Command{ if err != nil { pretty.Exit(1, "Cannot show tutorial text, reason: %v", err) } - common.Stdout("%s\n", content) + pretty.Page(content) }, } diff --git a/common/logger.go b/common/logger.go index 7ae78106..5e3f5039 100644 --- a/common/logger.go +++ b/common/logger.go @@ -64,20 +64,24 @@ func Error(context string, err error) { func Log(format string, details ...interface{}) { if !Silent { - printout(os.Stderr, fmt.Sprintf(format, details...)) + prefix := "" + if DebugFlag || TraceFlag { + prefix = "[N] " + } + printout(os.Stderr, fmt.Sprintf(prefix+format, details...)) } } func Debug(format string, details ...interface{}) error { if DebugFlag { - printout(os.Stderr, fmt.Sprintf(format, details...)) + printout(os.Stderr, fmt.Sprintf("[D] "+format, details...)) } return nil } func Trace(format string, details ...interface{}) error { if TraceFlag { - printout(os.Stderr, fmt.Sprintf(format, details...)) + printout(os.Stderr, fmt.Sprintf("[T] "+format, details...)) } return nil } diff --git a/common/version.go b/common/version.go index 2435bd74..a00a8e45 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.0.0` + Version = `v10.1.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 853a9267..c9404c28 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -35,13 +35,16 @@ func metaLoad(location string) (string, error) { if err != nil { return "", err } - return string(raw), nil + result := string(raw) + common.Trace("[Load] Metafile %q content: %s", location, result) + return result, nil } func metaSave(location, data string) error { if common.Stageonly { return nil } + common.Trace("[Save] Metafile %q content: %s", location, data) return ioutil.WriteFile(metafile(location), []byte(data), 0644) } @@ -56,10 +59,12 @@ func LastUsed(location string) (time.Time, error) { func IsPristine(folder string) bool { digest, err := DigestFor(folder, nil) if err != nil { + common.Trace("Calculating digest for folder %q failed, reason %v.", folder, err) return false } meta, err := metaLoad(folder) if err != nil { + common.Trace("Loading metafile for folder %q failed, reason %v.", folder, err) return false } return Hexdigest(digest) == meta @@ -172,7 +177,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { targetFolder := LiveFrom(key) planfile := fmt.Sprintf("%s.plan", targetFolder) - planWriter, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + planWriter, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return false, false } @@ -407,9 +412,12 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } else { templateFolder := TemplateFrom(key) if IsPristine(templateFolder) { + common.Trace("Template is pristine: %q", templateFolder) before := make(map[string]string) beforeHash, beforeErr := DigestFor(templateFolder, before) DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after, false) + } else { + common.Log("WARNING! Template is NOT pristine: %q", templateFolder) } common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) common.Timeline("2/6 base to live.") @@ -473,6 +481,7 @@ func RemoveEnvironment(label string) error { func removeClone(location string) error { if !pathlib.IsDir(location) { + common.Trace("Location %q is not directory, not removed.", location) return nil } randomLocation := fmt.Sprintf("%s.%08X", location, rand.Uint32()) @@ -482,19 +491,26 @@ func removeClone(location string) error { common.Log("Rename %q -> %q failed as: %v!", location, randomLocation, err) return err } + common.Trace("Rename %q -> %q was successful!", location, randomLocation) err = os.RemoveAll(randomLocation) if err != nil { common.Log("Removal of %q failed as: %v!", randomLocation, err) return err } + common.Trace("Removal of %q was successful!", randomLocation) meta := metafile(location) if pathlib.IsFile(meta) { - return os.Remove(meta) + err = os.Remove(meta) + common.Trace("Removal of %q result was %v.", meta, err) + return err } + common.Trace("Metafile %q was not file.", meta) return nil } func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { + common.Trace("Start CloneFromTo: %q -> %q", source, target) + defer common.Trace("Done CloneFromTo: %q -> %q", source, target) err := removeClone(target) if err != nil { return false, err @@ -509,11 +525,13 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { if err != nil { return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) } + common.Trace("Source %q dirty. And it was removed.", source) } return false, nil } expected, err := metaLoad(source) if err != nil { + common.Trace("Metafile load %q failed with %v.", source, err) return false, nil } success := cloneFolder(source, target, 100, copier) @@ -522,10 +540,17 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { if err != nil { return false, fmt.Errorf("Cloning %q to %q failed! And cleanup failed: %v", source, target, err) } + common.Trace("Cloing %q -> %q failed.", source, target) return false, nil } digest, err := DigestFor(target, nil) if err != nil || Hexdigest(digest) != expected { + common.Trace("Digest %q failed, %s vs %s or error %v.", target, Hexdigest(digest), expected, err) + origin := make(map[string]string) + originHash, originErr := DigestFor(source, origin) + created := make(map[string]string) + createdHash, createdErr := DigestFor(target, created) + DiagnoseDirty(source, target, originHash, createdHash, originErr, createdErr, origin, created, false) err = removeClone(target) if err != nil { return false, fmt.Errorf("Target %q does not match source %q! And cleanup failed: %v!", target, source, err) @@ -534,6 +559,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { } metaSave(target, expected) touchMetafile(source) + common.Trace("Success CloneFromTo: %q -> %q", source, target) return true, nil } diff --git a/docs/changelog.md b/docs/changelog.md index 338ef099..f0b283b2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # rcc change log +## v10.1.0 (date: 17.6.2021) + +- adding pager for `rcc man xxx` documents +- more trace printing on workflow setup +- added [D] and [T] markers for debug and trace level log entries +- when debug and trace log level is on, normal log entries are prefixed with [N] +- fixed rights problem in file `rcc_plan.log` + ## v10.0.0 (date: 15.6.2021) - removed lease support, this is major breaking change (if someone was using it) diff --git a/docs/recipes.md b/docs/recipes.md index 66ad2b4d..c2ed27d3 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -52,3 +52,23 @@ rcc task script --silent -- pip list # start interactive ipython session rcc task script --interactive -- ipython ``` + +## Where can I find updates for rcc? + +https://downloads.robocorp.com/rcc/releases/index.html + +That is rcc download site with two categories of: +- tested versions (these are ones we ship with our tools) +- latest 20 versions (which are not battle tested yet, but are bleeding edge) + +## What has changed on rcc? + +### See changelog from git repo ... + +https://github.com/robocorp/rcc/blob/master/docs/changelog.md + +### See that from your version of rcc directly ... + +```sh +rcc docs changelog +``` diff --git a/go.mod b/go.mod index 9539f306..33014c7e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,8 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.1 - golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 diff --git a/go.sum b/go.sum index 761a7041..b887f4e2 100644 --- a/go.sum +++ b/go.sum @@ -273,6 +273,11 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/htfs/commands.go b/htfs/commands.go index e81425cf..a299b047 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -95,7 +95,7 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e if force || !exists { identityfile := filepath.Join(tree.Stage(), "identity.yaml") - err = ioutil.WriteFile(identityfile, blueprint, 0o640) + err = ioutil.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) label, err := conda.NewEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 304d55ab..9f6a5fc7 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -227,7 +227,7 @@ func fileIt(filename string) (io.WriteCloser, error) { if len(filename) == 0 { return os.Stdout, nil } - file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return nil, err } diff --git a/operations/running.go b/operations/running.go index 86ab72a5..db888b0c 100644 --- a/operations/running.go +++ b/operations/running.go @@ -154,7 +154,7 @@ func ExecuteSimpleTask(flags *RunFlags, template []string, config robot.Robot, t } } outputDir := config.ArtifactDirectory() - common.Debug("DEBUG: about to run command - %v", task) + common.Debug("about to run command - %v", task) if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) } else { @@ -216,7 +216,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { PipFreeze(searchPath, directory, outputDir, environment) } - common.Debug("DEBUG: about to run command - %v", task) + common.Debug("about to run command - %v", task) if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) } else { diff --git a/pretty/pager.go b/pretty/pager.go new file mode 100644 index 00000000..69be71cb --- /dev/null +++ b/pretty/pager.go @@ -0,0 +1,59 @@ +package pretty + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + + "github.com/robocorp/rcc/common" + "golang.org/x/term" +) + +var ( + titlePattern = regexp.MustCompile("^#{1,5}\\s+") + codePattern = regexp.MustCompile("^ {4,}\\S+") + blockPattern = regexp.MustCompile("^```") +) + +func Page(content []byte) { + width, height, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || !Interactive { + common.Stdout("\n%s\n", content) + return + } + + titleStyle := fmt.Sprintf("%s%s", Bold, Underline) + codeStyle := Faint + + limit := height - 3 + reader := bufio.NewReader(os.Stdin) + lines := strings.SplitAfter(string(content), "\n") + fmt.Printf("%s%s", Home, Clear) + row := 0 + block := false + for _, line := range lines { + flat := strings.TrimRight(line, " \t\r\n") + if blockPattern.MatchString(flat) { + block = !block + continue + } + adjust := len(flat) / width + row += 1 + adjust + if row > limit { + fmt.Print("\n-- press enter to continue or ctrl-c to stop --") + reader.ReadLine() + fmt.Printf("%s%s", Home, Clear) + row = 1 + } + style := "" + if titlePattern.MatchString(flat) { + style = titleStyle + } + if block || codePattern.MatchString(flat) { + style = codeStyle + } + fmt.Printf("%s%s%s\n", style, flat, Reset) + } +} diff --git a/pretty/variables.go b/pretty/variables.go index 1e6ffaeb..36c67fee 100644 --- a/pretty/variables.go +++ b/pretty/variables.go @@ -22,6 +22,12 @@ var ( Reset string Sparkles string Rocket string + Home string + Clear string + Bold string + Faint string + Italic string + Underline string ) func Setup() { @@ -42,6 +48,12 @@ func Setup() { Cyan = csi("96m") Yellow = csi("93m") Reset = csi("0m") + Home = csi("1;1H") + Clear = csi("0J") + Bold = csi("1m") + Faint = csi("2m") + Italic = csi("3m") + Underline = csi("4m") } if Iconic && !Colorless { Sparkles = "\u2728 " From b9041ab0a104fecd5d241b7a6942da3fe4068c45 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 18 Jun 2021 10:27:10 +0300 Subject: [PATCH 150/516] UPGRADE: new micromamba into use (v10.1.1) - taking micromamba 0.14.0 into use --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 4 ++++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index a00a8e45..4487dd4a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.1.0` + Version = `v10.1.1` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 317d6785..1a2c755b 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.13.1/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.14.0/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index e90d33b7..95ea7a39 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.13.1/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.14.0/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index bd8a3684..bf9fd373 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.13.1/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.14.0/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 00f31f56..92e09729 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -233,7 +233,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 13001 + goodEnough := version >= 14000 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index f0b283b2..6a026116 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.1.1 (date: 18.6.2021) + +- taking micromamba 0.14.0 into use + ## v10.1.0 (date: 17.6.2021) - adding pager for `rcc man xxx` documents From 5d8c0fdae081fe47754f44036d8614336c8dd19b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 21 Jun 2021 09:35:51 +0300 Subject: [PATCH 151/516] RCC-149: robot dependency listing (v10.2.0) - adding golden-ee.yaml document into holotree space (listing of components) --- common/version.go | 2 +- conda/dependencies.go | 80 +++++++++++++++++++++++++++++++++++++++++++ conda/workflows.go | 9 +++-- docs/changelog.md | 4 +++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 conda/dependencies.go diff --git a/common/version.go b/common/version.go index 4487dd4a..2cd42060 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.1.1` + Version = `v10.2.0` ) diff --git a/conda/dependencies.go b/conda/dependencies.go new file mode 100644 index 00000000..3cfafaf5 --- /dev/null +++ b/conda/dependencies.go @@ -0,0 +1,80 @@ +package conda + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/fail" + "github.com/robocorp/rcc/pretty" + "gopkg.in/yaml.v2" +) + +type dependency struct { + Name string `yaml:"name" json:"name"` + Version string `yaml:"version" json:"version"` + Origin string `yaml:"origin" json:"channel"` +} + +type dependencies []*dependency + +func parseDependencies(origin string, output []byte) (dependencies, error) { + result := make(dependencies, 0, 100) + err := json.Unmarshal(output, &result) + if err != nil { + return nil, err + } + if len(origin) == 0 { + return result, nil + } + for _, here := range result { + if len(here.Origin) == 0 { + here.Origin = origin + } + } + return result, nil +} + +func fillDependencies(context, targetFolder string, seen map[string]string, collector dependencies, command ...string) (_ dependencies, err error) { + defer fail.Around(&err) + + task, err := livePrepare(targetFolder, command...) + fail.On(err != nil, "%v", err) + out, _, err := task.CaptureOutput() + fail.On(err != nil, "%v", err) + listing, err := parseDependencies(context, []byte(out)) + fail.On(err != nil, "%v", err) + for _, entry := range listing { + found, ok := seen[strings.ToLower(entry.Name)] + if ok && found == entry.Version { + continue + } + collector = append(collector, entry) + seen[strings.ToLower(entry.Name)] = entry.Version + } + return collector, nil +} + +func goldenMaster(targetFolder string, pipUsed bool) (err error) { + defer fail.Around(&err) + + seen := make(map[string]string) + collector := make(dependencies, 0, 100) + collector, err = fillDependencies("mamba", targetFolder, seen, collector, BinMicromamba(), "list", "--json") + fail.On(err != nil, "Failed to list micromamba dependencies, reason: %v", err) + if pipUsed { + collector, err = fillDependencies("pypi", targetFolder, seen, collector, "pip", "list", "--isolated", "--local", "--format", "json") + fail.On(err != nil, "Failed to list pip dependencies, reason: %v", err) + } + sort.SliceStable(collector, func(left, right int) bool { + return strings.ToLower(collector[left].Name) < strings.ToLower(collector[right].Name) + }) + body, err := yaml.Marshal(collector) + fail.On(err != nil, "Failed to make yaml, reason: %v", err) + goldenfile := filepath.Join(targetFolder, "golden-ee.yaml") + common.Debug("%sGolden EE file at: %v%s", pretty.Yellow, goldenfile, pretty.Reset) + return os.WriteFile(goldenfile, body, 0644) +} diff --git a/conda/workflows.go b/conda/workflows.go index c9404c28..2438f889 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -218,7 +218,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, true } fmt.Fprintf(planWriter, "\n--- pip plan @%ss ---\n\n", stopwatch) - pipCache, wheelCache := common.PipCache(), common.WheelCache() + pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { common.Log("#### Progress: 4/6 [pip install phase skipped -- no pip dependencies]") @@ -227,7 +227,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh common.Log("#### Progress: 4/6 [pip install phase]") common.Timeline("4/6 pip install start.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) - pipCommand := common.NewCommander("pip", "install", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) + pipCommand := common.NewCommander("pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -239,6 +239,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } common.Timeline("pip done.") + pipUsed = true } fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { @@ -269,6 +270,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh for _, line := range LoadActivationEnvironment(targetFolder) { fmt.Fprintf(planWriter, "%s\n", line) } + err = goldenMaster(targetFolder, pipUsed) + if err != nil { + common.Log("%sGolden EE failure: %v%s", pretty.Yellow, err, pretty.Reset) + } fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planWriter.Sync() planWriter.Close() diff --git a/docs/changelog.md b/docs/changelog.md index 6a026116..c1db5289 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.2.0 (date: 21.6.2021) + +- adding golden-ee.yaml document into holotree space (listing of components) + ## v10.1.1 (date: 18.6.2021) - taking micromamba 0.14.0 into use From c9d74aa4459ab06fcaf709b555ad94f852ad03f9 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 21 Jun 2021 13:03:38 +0300 Subject: [PATCH 152/516] RCC-149: robot dependency listing (v10.2.1) - showing dependencies listing from environment before runs --- common/version.go | 2 +- conda/dependencies.go | 37 ++++++++++++++++++++++++++++++++++++- docs/changelog.md | 4 ++++ operations/running.go | 34 +++++++++++++++++++--------------- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/common/version.go b/common/version.go index 2cd42060..ec5a2388 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.2.0` + Version = `v10.2.1` ) diff --git a/conda/dependencies.go b/conda/dependencies.go index 3cfafaf5..a3449e56 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -2,10 +2,12 @@ package conda import ( "encoding/json" + "fmt" "os" "path/filepath" "sort" "strings" + "text/tabwriter" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/fail" @@ -58,6 +60,10 @@ func fillDependencies(context, targetFolder string, seen map[string]string, coll return collector, nil } +func goldenMasterFilename(targetFolder string) string { + return filepath.Join(targetFolder, "golden-ee.yaml") +} + func goldenMaster(targetFolder string, pipUsed bool) (err error) { defer fail.Around(&err) @@ -74,7 +80,36 @@ func goldenMaster(targetFolder string, pipUsed bool) (err error) { }) body, err := yaml.Marshal(collector) fail.On(err != nil, "Failed to make yaml, reason: %v", err) - goldenfile := filepath.Join(targetFolder, "golden-ee.yaml") + goldenfile := goldenMasterFilename(targetFolder) common.Debug("%sGolden EE file at: %v%s", pretty.Yellow, goldenfile, pretty.Reset) return os.WriteFile(goldenfile, body, 0644) } + +func LoadWantedDependencies(filename string) (_ dependencies, err error) { + defer fail.Around(&err) + + body, err := os.ReadFile(filename) + fail.On(err != nil, "Failed to read dependencies from %q, reason: %v.", filename, err) + result := make(dependencies, 0, 100) + err = yaml.Unmarshal(body, &result) + fail.On(err != nil, "Failed to parse dependencies from %q, reason: %v.", filename, err) + return result, nil +} + +func DumpEnvironmentDependencies(targetFolder string) (err error) { + defer fail.Around(&err) + + filename := goldenMasterFilename(targetFolder) + dependencyList, err := LoadWantedDependencies(filename) + fail.On(err != nil, "%v", err) + + tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) + tabbed.Write([]byte("No.\tPackage\tVersion\tOrigin\n")) + tabbed.Write([]byte("---\t-------\t-------\t------\n")) + for at, entry := range dependencyList { + data := fmt.Sprintf("%3d\t%s\t%s\t%s\n", at+1, entry.Name, entry.Version, entry.Origin) + tabbed.Write([]byte(data)) + } + tabbed.Flush() + return nil +} diff --git a/docs/changelog.md b/docs/changelog.md index c1db5289..8d0be1f4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.2.1 (date: 21.6.2021) + +- showing dependencies listing from environment before runs + ## v10.2.0 (date: 21.6.2021) - adding golden-ee.yaml document into holotree space (listing of components) diff --git a/operations/running.go b/operations/running.go index db888b0c..10652726 100644 --- a/operations/running.go +++ b/operations/running.go @@ -29,25 +29,29 @@ type RunFlags struct { NoPipFreeze bool } -func PipFreeze(searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { - pip, ok := searchPath.Which("pip", conda.FileExtensions) - if !ok { - return false - } - fullPip, err := filepath.EvalSymlinks(pip) +func ExecutionEnvironmentListing(label string, searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { + common.Timeline("execution environment listing") + defer common.Log("--") + err := conda.DumpEnvironmentDependencies(label) if err != nil { - return false - } - common.Log("Installed pip packages:") - if common.NoOutputCapture { - _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Execute(false) - } else { - _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Tee(outputDir, false) + pip, ok := searchPath.Which("pip", conda.FileExtensions) + if !ok { + return false + } + fullPip, err := filepath.EvalSymlinks(pip) + if err != nil { + return false + } + common.Log("Installed pip packages:") + if common.NoOutputCapture { + _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Execute(false) + } else { + _, err = shell.New(environment, directory, fullPip, "freeze", "--all").Tee(outputDir, false) + } } if err != nil { return false } - common.Log("--") return true } @@ -214,7 +218,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro beforeHash, beforeErr := conda.DigestFor(label, before) outputDir := config.ArtifactDirectory() if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { - PipFreeze(searchPath, directory, outputDir, environment) + ExecutionEnvironmentListing(label, searchPath, directory, outputDir, environment) } common.Debug("about to run command - %v", task) if common.NoOutputCapture { From 66c2fb70044740b22082cc3a751c9404c902f99e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 23 Jun 2021 11:59:30 +0300 Subject: [PATCH 153/516] RCC-149: robot dependency listing (v10.2.2) - adding `rcc robot dependencies` command for viewing desired execution environment dependencies - same view is now also shown in run context replacing `pip freeze` if golden-ee.yaml exists in execution environment --- cmd/robotdependencies.go | 63 ++++++++++++++++++++++ common/version.go | 2 +- conda/dependencies.go | 113 ++++++++++++++++++++++++++++++++------- docs/changelog.md | 7 +++ docs/recipes.md | 42 +++++++++++++++ operations/running.go | 10 ++-- robot/robot.go | 5 ++ 7 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 cmd/robotdependencies.go diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go new file mode 100644 index 00000000..eeccd7b0 --- /dev/null +++ b/cmd/robotdependencies.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" + "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" + "github.com/robocorp/rcc/robot" + + "github.com/spf13/cobra" +) + +var ( + copyDependenciesFlag bool +) + +func doShowDependencies(config robot.Robot, label string) { + filename, _ := config.DependenciesFile() + err := conda.SideBySideViewOfDependencies(conda.GoldenMasterFilename(label), filename) + pretty.Guard(err == nil, 3, "Failed to show dependencies, reason: %v", err) +} + +func doCopyDependencies(config robot.Robot, label string) { + mode := "[create]" + target, found := config.DependenciesFile() + if found { + mode = "[overwrite]" + } + source := conda.GoldenMasterFilename(label) + common.Log("%sCopying %q as wanted %q %s.%s", pretty.Yellow, source, target, mode, pretty.Reset) + err := pathlib.CopyFile(source, target, found) + pretty.Guard(err == nil, 2, "Copy %q -> %q failed, reason: %v", source, target, err) +} + +var robotDependenciesCmd = &cobra.Command{ + Use: "dependencies", + Short: "View wanted vs. available dependencies of robot execution environment.", + Long: "View wanted vs. available dependencies of robot execution environment.", + Aliases: []string{"deps"}, + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Robot dependencies run lasted").Report() + } + simple, config, _, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) + pretty.Guard(!simple, 1, "Cannot view dependencies of simple robots.") + if copyDependenciesFlag { + common.Log("--") + doCopyDependencies(config, label) + } + common.Log("--") + doShowDependencies(config, label) + pretty.Ok() + }, +} + +func init() { + robotCmd.AddCommand(robotDependenciesCmd) + robotDependenciesCmd.Flags().BoolVarP(©DependenciesFlag, "copy", "c", false, "Copy golden-ee.yaml from environment as wanted dependencies.yaml, overwriting previous if exists.") + robotDependenciesCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Forced environment update.") + robotDependenciesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") + robotDependenciesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Space to use for execution environment dependencies.") +} diff --git a/common/version.go b/common/version.go index ec5a2388..e06459da 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.2.1` + Version = `v10.2.2` ) diff --git a/conda/dependencies.go b/conda/dependencies.go index a3449e56..9ce1b22c 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -21,8 +21,24 @@ type dependency struct { Origin string `yaml:"origin" json:"channel"` } +func (it *dependency) AsKey() string { + return fmt.Sprintf("%-50s %-20s", it.Name, it.Origin) +} + type dependencies []*dependency +func (it dependencies) sorted() dependencies { + sort.SliceStable(it, func(left, right int) bool { + lefty := strings.ToLower(it[left].Name) + righty := strings.ToLower(it[right].Name) + if lefty == righty { + return it[left].Origin < it[right].Origin + } + return lefty < righty + }) + return it +} + func parseDependencies(origin string, output []byte) (dependencies, error) { result := make(dependencies, 0, 100) err := json.Unmarshal(output, &result) @@ -60,7 +76,7 @@ func fillDependencies(context, targetFolder string, seen map[string]string, coll return collector, nil } -func goldenMasterFilename(targetFolder string) string { +func GoldenMasterFilename(targetFolder string) string { return filepath.Join(targetFolder, "golden-ee.yaml") } @@ -75,41 +91,98 @@ func goldenMaster(targetFolder string, pipUsed bool) (err error) { collector, err = fillDependencies("pypi", targetFolder, seen, collector, "pip", "list", "--isolated", "--local", "--format", "json") fail.On(err != nil, "Failed to list pip dependencies, reason: %v", err) } - sort.SliceStable(collector, func(left, right int) bool { - return strings.ToLower(collector[left].Name) < strings.ToLower(collector[right].Name) - }) - body, err := yaml.Marshal(collector) + body, err := yaml.Marshal(collector.sorted()) fail.On(err != nil, "Failed to make yaml, reason: %v", err) - goldenfile := goldenMasterFilename(targetFolder) + goldenfile := GoldenMasterFilename(targetFolder) common.Debug("%sGolden EE file at: %v%s", pretty.Yellow, goldenfile, pretty.Reset) return os.WriteFile(goldenfile, body, 0644) } -func LoadWantedDependencies(filename string) (_ dependencies, err error) { - defer fail.Around(&err) - +func LoadWantedDependencies(filename string) dependencies { body, err := os.ReadFile(filename) - fail.On(err != nil, "Failed to read dependencies from %q, reason: %v.", filename, err) + if err != nil { + return dependencies{} + } result := make(dependencies, 0, 100) err = yaml.Unmarshal(body, &result) - fail.On(err != nil, "Failed to parse dependencies from %q, reason: %v.", filename, err) - return result, nil + if err != nil { + return dependencies{} + } + return result.sorted() } -func DumpEnvironmentDependencies(targetFolder string) (err error) { +func SideBySideViewOfDependencies(goldenfile, wantedfile string) (err error) { defer fail.Around(&err) - filename := goldenMasterFilename(targetFolder) - dependencyList, err := LoadWantedDependencies(filename) - fail.On(err != nil, "%v", err) + gold := LoadWantedDependencies(goldenfile) + want := LoadWantedDependencies(wantedfile) + + if len(gold) == 0 && len(want) == 0 { + return fmt.Errorf("Running against old environment, and no dependencies.yaml.") + } + diffmap := make(map[string][2]int) + injectDiffmap(diffmap, want, 0) + injectDiffmap(diffmap, gold, 1) + keyset := make([]string, 0, len(diffmap)) + for key, _ := range diffmap { + keyset = append(keyset, key) + } + sort.Strings(keyset) + + common.WaitLogs() + hasgold := false + unknown := fmt.Sprintf("%sUnknown%s", pretty.Grey, pretty.Reset) + same := fmt.Sprintf("%sSame%s", pretty.Cyan, pretty.Reset) + drifted := fmt.Sprintf("%sDrifted%s", pretty.Yellow, pretty.Reset) + missing := fmt.Sprintf("%sN/A%s", pretty.Grey, pretty.Reset) tabbed := tabwriter.NewWriter(os.Stderr, 2, 4, 2, ' ', 0) - tabbed.Write([]byte("No.\tPackage\tVersion\tOrigin\n")) - tabbed.Write([]byte("---\t-------\t-------\t------\n")) - for at, entry := range dependencyList { - data := fmt.Sprintf("%3d\t%s\t%s\t%s\n", at+1, entry.Name, entry.Version, entry.Origin) + tabbed.Write([]byte("Wanted\tVersion\tOrigin\t|\tNo.\t|\tAvailable\tVersion\tOrigin\t|\tStatus\n")) + tabbed.Write([]byte("------\t-------\t------\t+\t---\t+\t---------\t-------\t------\t+\t------\n")) + for at, key := range keyset { + //left, right, status := "n/a\tn/a\tn/a\t", "\tn/a\tn/a\tn/a", unknown + left, right, status := "-\t-\t-\t", "\t-\t-\t-", unknown + sides := diffmap[key] + if sides[0] < 0 || sides[1] < 0 { + status = missing + } else { + left, right := want[sides[0]], gold[sides[1]] + if left.Version == right.Version { + status = same + } else { + status = drifted + } + } + if sides[0] > -1 { + entry := want[sides[0]] + left = fmt.Sprintf("%s\t%s\t%s\t", entry.Name, entry.Version, entry.Origin) + } + if sides[1] > -1 { + entry := gold[sides[1]] + right = fmt.Sprintf("\t%s\t%s\t%s", entry.Name, entry.Version, entry.Origin) + hasgold = true + } + data := fmt.Sprintf("%s|\t%3d\t|%s\t|\t%s\n", left, at+1, right, status) tabbed.Write([]byte(data)) } + tabbed.Write([]byte("------\t-------\t------\t+\t---\t+\t---------\t-------\t------\t+\t------\n")) + tabbed.Write([]byte("Wanted\tVersion\tOrigin\t|\tNo.\t|\tAvailable\tVersion\tOrigin\t|\tStatus\n")) + tabbed.Write([]byte("\n")) tabbed.Flush() + if !hasgold { + return fmt.Errorf("Running against old environment, which does not have 'golden-ee.yaml' file.") + } return nil } + +func injectDiffmap(diffmap map[string][2]int, deps dependencies, side int) { + for at, entry := range deps { + key := entry.AsKey() + found, ok := diffmap[key] + if !ok { + found = [2]int{-1, -1} + } + found[side] = at + diffmap[key] = found + } +} diff --git a/docs/changelog.md b/docs/changelog.md index 8d0be1f4..b5d5b7d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v10.2.2 (date: 23.6.2021) + +- adding `rcc robot dependencies` command for viewing desired execution + environment dependencies +- same view is now also shown in run context replacing `pip freeze` if + golden-ee.yaml exists in execution environment + ## v10.2.1 (date: 21.6.2021) - showing dependencies listing from environment before runs diff --git a/docs/recipes.md b/docs/recipes.md index c2ed27d3..a133b1d9 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -1,5 +1,47 @@ # Tips, tricks, and recipies +## How to see dependency changes? + +Since version 10.2.2, rcc can show dependency listings using +`rcc robot dependencies` command. Listing always have two sided, "Wanted" +which is content from dependencies.yaml file, and "Available" which is from +actual environment command was run against. Listing is also shown during +robot runs. + +### Why is this important? + +- as time passes and world moves forward, new version of used components + (dependencies) are released, and this may cause "configuration drift" on + your robots, and without tooling in place, this drift might go unnoticed +- if your dependencies are not fixed, there will be configuration drift and + your robot may change behaviour (become buggy) when dependency changes and + goes against implemented robot +- even if you fix your dependencies in `conda.yaml`, some of those components + or their components might have floating dependencies and they change your + robots behaviour +- if your execution environment is different from your development environment + then there might be different versions available for different operating + systems +- if dependency resolution algorithm changes (pip for example) then you might + get different environment with same `conda.yaml` +- when you upgrade one of your dependencies (for example, rpaframework) to new + version, dependency resolution will now change, and now listing helps you + understand what has changed and how you need to change your robot + implementation because of that + +### Example of dependencies listing from holotree environment + +```sh +# first list dependencies from execution environment +rcc robot dependencies --space user + +# if everything looks good, copy it as wanted dependencies.yaml +rcc robot dependencies --space user --copy + +# and verify that everything looks `Same` +rcc robot dependencies --space user +``` + ## How pass arguments to robot from CLI? Since version 9.15.0, rcc supports passing arguments from CLI to underlying diff --git a/operations/running.go b/operations/running.go index 10652726..e4c2975f 100644 --- a/operations/running.go +++ b/operations/running.go @@ -29,10 +29,11 @@ type RunFlags struct { NoPipFreeze bool } -func ExecutionEnvironmentListing(label string, searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { +func ExecutionEnvironmentListing(wantedfile, label string, searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { common.Timeline("execution environment listing") defer common.Log("--") - err := conda.DumpEnvironmentDependencies(label) + goldenfile := conda.GoldenMasterFilename(label) + err := conda.SideBySideViewOfDependencies(goldenfile, wantedfile) if err != nil { pip, ok := searchPath.Which("pip", conda.FileExtensions) if !ok { @@ -57,7 +58,7 @@ func ExecutionEnvironmentListing(label string, searchPath pathlib.PathParts, dir func LoadAnyTaskEnvironment(packfile string, force bool) (bool, robot.Robot, robot.Task, string) { FixRobot(packfile) - config, err := robot.LoadRobotYaml(packfile, true) + config, err := robot.LoadRobotYaml(packfile, false) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -218,7 +219,8 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro beforeHash, beforeErr := conda.DigestFor(label, before) outputDir := config.ArtifactDirectory() if !flags.NoPipFreeze && !flags.Assistant && !common.Silent && !interactive { - ExecutionEnvironmentListing(label, searchPath, directory, outputDir, environment) + wantedfile, _ := config.DependenciesFile() + ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } common.Debug("about to run command - %v", task) if common.NoOutputCapture { diff --git a/robot/robot.go b/robot/robot.go index 1a91a99c..b74ed61f 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -31,6 +31,7 @@ type Robot interface { Holozip() string Validate() (bool, error) Diagnostics(*common.DiagnosticStatus, bool) + DependenciesFile() (string, bool) WorkingDirectory() string ArtifactDirectory() string @@ -202,6 +203,10 @@ func (it *robot) Validate() (bool, error) { } return true, nil } +func (it *robot) DependenciesFile() (string, bool) { + filename := filepath.Join(it.Root, "dependencies.yaml") + return filename, pathlib.IsFile(filename) +} func (it *robot) RootDirectory() string { return it.Root From c56da4980f9b8edd35e6ed50a4e824a9bb98cd31 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 24 Jun 2021 11:24:46 +0300 Subject: [PATCH 154/516] RCC-149: robot dependency listing (v10.2.3) - added `dependencies.yaml` into robot diagnostics - show ideal `conda.yaml` that matches `dependencies.yaml` - fixed `--force` install on base/live environments --- cmd/robotdependencies.go | 7 +++++ common/version.go | 2 +- conda/condayaml.go | 46 +++++++++++++++++++++++++++++++++ conda/dependencies.go | 15 +++++++++++ conda/workflows.go | 12 ++++++--- docs/changelog.md | 6 +++++ robot/robot.go | 56 +++++++++++++++++++++++++++++++++++++++- 7 files changed, 138 insertions(+), 6 deletions(-) diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index eeccd7b0..d7aac1b8 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -33,6 +33,12 @@ func doCopyDependencies(config robot.Robot, label string) { pretty.Guard(err == nil, 2, "Copy %q -> %q failed, reason: %v", source, target, err) } +func doShowIdeal(config robot.Robot, label string) { + ideal, ok := config.IdealCondaYaml() + pretty.Guard(ok, 4, "Could not determine ideal conda.yaml. Sorry.") + common.Log("Ideal conda.yaml based on 'dependencies.yaml' would be:\n%s", ideal) +} + var robotDependenciesCmd = &cobra.Command{ Use: "dependencies", Short: "View wanted vs. available dependencies of robot execution environment.", @@ -50,6 +56,7 @@ var robotDependenciesCmd = &cobra.Command{ } common.Log("--") doShowDependencies(config, label) + doShowIdeal(config, label) pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index e06459da..0fb8d9c9 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.2.2` + Version = `v10.2.3` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 2b1bba9e..198032b2 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -134,6 +134,52 @@ func SummonEnvironment(filename string) *Environment { } } +func (it *Environment) FromDependencies(fixed dependencies) (*Environment, bool) { + result := &Environment{ + Name: it.Name, + Prefix: it.Prefix, + Channels: it.Channels, + Conda: []*Dependency{}, + Pip: []*Dependency{}, + } + same := true + for _, dependency := range it.Conda { + found, ok := fixed.Lookup(dependency.Name, false) + if !ok { + result.Conda = append(result.Conda, dependency) + same = false + continue + } + if dependency.Qualifier != "=" || dependency.Versions != found.Version { + same = false + } + result.Conda = append(result.Conda, &Dependency{ + Original: fmt.Sprintf("%s=%s", dependency.Name, found.Version), + Name: dependency.Name, + Qualifier: "=", + Versions: found.Version, + }) + } + for _, dependency := range it.Pip { + found, ok := fixed.Lookup(dependency.Name, true) + if !ok { + result.Conda = append(result.Conda, dependency) + same = false + continue + } + if dependency.Qualifier != "==" || dependency.Versions != found.Version { + same = false + } + result.Pip = append(result.Pip, &Dependency{ + Original: fmt.Sprintf("%s==%s", dependency.Name, found.Version), + Name: dependency.Name, + Qualifier: "==", + Versions: found.Version, + }) + } + return result, same +} + func (it *Environment) pipPromote() error { removed := make([]int, 0, len(it.Pip)) diff --git a/conda/dependencies.go b/conda/dependencies.go index 9ce1b22c..06347037 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -39,6 +39,21 @@ func (it dependencies) sorted() dependencies { return it } +func (it dependencies) Lookup(name string, pypi bool) (*dependency, bool) { + for _, entry := range it { + if pypi && entry.Origin != "pypi" { + continue + } + if !pypi && entry.Origin == "pypi" { + continue + } + if entry.Name == name { + return entry, true + } + } + return nil, false +} + func parseDependencies(origin string, output []byte) (dependencies, error) { result := make(dependencies, 0, 100) err := json.Unmarshal(output, &result) diff --git a/conda/workflows.go b/conda/workflows.go index 2438f889..f8e362d0 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -406,14 +406,18 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { if err != nil { return "", err } - if reusable { + if !force && reusable { hits += 1 xviper.Set("stats.env.hit", hits) return liveFolder, nil } - if common.Stageonly { - common.Log("#### Progress: 2/6 [skipped -- stage only]") - common.Timeline("2/6 stage only.") + if force || common.Stageonly { + if force { + common.Log("#### Progress: 2/6 [skipped -- forced]") + } else { + common.Log("#### Progress: 2/6 [skipped -- stage only]") + common.Timeline("2/6 stage only.") + } } else { templateFolder := TemplateFrom(key) if IsPristine(templateFolder) { diff --git a/docs/changelog.md b/docs/changelog.md index b5d5b7d4..22bd6470 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v10.2.3 (date: 24.6.2021) + +- added `dependencies.yaml` into robot diagnostics +- show ideal `conda.yaml` that matches `dependencies.yaml` +- fixed `--force` install on base/live environments + ## v10.2.2 (date: 23.6.2021) - adding `rcc robot dependencies` command for viewing desired execution diff --git a/robot/robot.go b/robot/robot.go index b74ed61f..c28bedf8 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -32,6 +32,7 @@ type Robot interface { Validate() (bool, error) Diagnostics(*common.DiagnosticStatus, bool) DependenciesFile() (string, bool) + IdealCondaYaml() (string, bool) WorkingDirectory() string ArtifactDirectory() string @@ -173,7 +174,15 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { target.Details["robot-artifact-directory"] = it.ArtifactDirectory() target.Details["robot-paths"] = strings.Join(it.Paths(), ", ") target.Details["robot-python-paths"] = strings.Join(it.PythonPaths(), ", ") - + dependencies, ok := it.DependenciesFile() + if !ok { + dependencies = "missing" + } else { + if it.VerifyCondaDependencies() { + diagnose.Ok("Dependencies in conda.yaml and dependencies.yaml match.") + } + } + target.Details["robot-dependencies-yaml"] = dependencies } func (it *robot) Validate() (bool, error) { @@ -203,11 +212,56 @@ func (it *robot) Validate() (bool, error) { } return true, nil } + func (it *robot) DependenciesFile() (string, bool) { filename := filepath.Join(it.Root, "dependencies.yaml") return filename, pathlib.IsFile(filename) } +func (it *robot) IdealCondaYaml() (string, bool) { + wanted, ok := it.DependenciesFile() + if !ok { + return "", false + } + dependencies := conda.LoadWantedDependencies(wanted) + if len(dependencies) == 0 { + return "", false + } + condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) + if err != nil { + return "", false + } + ideal, _ := condaEnv.FromDependencies(dependencies) + body, err := ideal.AsYaml() + if err != nil { + return "", false + } + return body, true +} + +func (it *robot) VerifyCondaDependencies() bool { + wanted, ok := it.DependenciesFile() + if !ok { + return true + } + dependencies := conda.LoadWantedDependencies(wanted) + if len(dependencies) == 0 { + return true + } + condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) + if err != nil { + return true + } + ideal, ok := condaEnv.FromDependencies(dependencies) + if !ok { + body, err := ideal.AsYaml() + if err == nil { + fmt.Println("IDEAL:", body) + } + } + return ok +} + func (it *robot) RootDirectory() string { return it.Root } From 65ad076f09a3f153a50b282860e809315c9d1282 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 24 Jun 2021 15:57:43 +0300 Subject: [PATCH 155/516] RCC-149: robot dependency listing (v10.2.4) - added `--bind` option to copy exact dependencies from `dependencies.yaml` into `conda.yaml`, so that `conda.yaml` represents fixed dependencies --- cmd/robotdependencies.go | 18 +++++++++++++++++- common/version.go | 2 +- conda/condayaml.go | 10 +++------- docs/changelog.md | 5 +++++ docs/recipes.md | 4 ++++ robot/robot.go | 5 ++++- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index d7aac1b8..a0b44ae4 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" @@ -13,6 +15,7 @@ import ( var ( copyDependenciesFlag bool + bindDependenciesFlag bool ) func doShowDependencies(config robot.Robot, label string) { @@ -33,9 +36,18 @@ func doCopyDependencies(config robot.Robot, label string) { pretty.Guard(err == nil, 2, "Copy %q -> %q failed, reason: %v", source, target, err) } +func doBindDependencies(config robot.Robot, label string) { + ideal, ok := config.IdealCondaYaml() + pretty.Guard(ok, 4, "Could not determine ideal conda.yaml for binding. Sorry.") + filename := config.CondaConfigFile() + err := os.WriteFile(filename, []byte(ideal), 0644) + pretty.Guard(err == nil, 5, "Could not write file %q, reason: %v", filename, err) + common.Log("%sOverwrote %q with ideal conda.yaml content.%s", pretty.Yellow, filename, pretty.Reset) +} + func doShowIdeal(config robot.Robot, label string) { ideal, ok := config.IdealCondaYaml() - pretty.Guard(ok, 4, "Could not determine ideal conda.yaml. Sorry.") + pretty.Guard(ok, 6, "Could not determine ideal conda.yaml. Sorry.") common.Log("Ideal conda.yaml based on 'dependencies.yaml' would be:\n%s", ideal) } @@ -56,6 +68,9 @@ var robotDependenciesCmd = &cobra.Command{ } common.Log("--") doShowDependencies(config, label) + if bindDependenciesFlag { + doBindDependencies(config, label) + } doShowIdeal(config, label) pretty.Ok() }, @@ -64,6 +79,7 @@ var robotDependenciesCmd = &cobra.Command{ func init() { robotCmd.AddCommand(robotDependenciesCmd) robotDependenciesCmd.Flags().BoolVarP(©DependenciesFlag, "copy", "c", false, "Copy golden-ee.yaml from environment as wanted dependencies.yaml, overwriting previous if exists.") + robotDependenciesCmd.Flags().BoolVarP(&bindDependenciesFlag, "bind", "b", false, "Bind (overwrite) conda.yaml dependencies from 'dependencies.yaml'.") robotDependenciesCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Forced environment update.") robotDependenciesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") robotDependenciesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Space to use for execution environment dependencies.") diff --git a/common/version.go b/common/version.go index 0fb8d9c9..30d73e78 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.2.3` + Version = `v10.2.4` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 198032b2..85c528ff 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -148,11 +148,9 @@ func (it *Environment) FromDependencies(fixed dependencies) (*Environment, bool) if !ok { result.Conda = append(result.Conda, dependency) same = false + common.Debug("Could not fix version for dependency %q from conda.", dependency.Name) continue } - if dependency.Qualifier != "=" || dependency.Versions != found.Version { - same = false - } result.Conda = append(result.Conda, &Dependency{ Original: fmt.Sprintf("%s=%s", dependency.Name, found.Version), Name: dependency.Name, @@ -163,13 +161,11 @@ func (it *Environment) FromDependencies(fixed dependencies) (*Environment, bool) for _, dependency := range it.Pip { found, ok := fixed.Lookup(dependency.Name, true) if !ok { - result.Conda = append(result.Conda, dependency) + result.Conda = append(result.Pip, dependency) same = false + common.Debug("Could not fix version for dependency %q from pypi.", dependency.Name) continue } - if dependency.Qualifier != "==" || dependency.Versions != found.Version { - same = false - } result.Pip = append(result.Pip, &Dependency{ Original: fmt.Sprintf("%s==%s", dependency.Name, found.Version), Name: dependency.Name, diff --git a/docs/changelog.md b/docs/changelog.md index 22bd6470..0a33fee5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.2.4 (date: 24.6.2021) + +- added `--bind` option to copy exact dependencies from `dependencies.yaml` + into `conda.yaml`, so that `conda.yaml` represents fixed dependencies + ## v10.2.3 (date: 24.6.2021) - added `dependencies.yaml` into robot diagnostics diff --git a/docs/recipes.md b/docs/recipes.md index a133b1d9..a2dc634e 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -40,6 +40,10 @@ rcc robot dependencies --space user --copy # and verify that everything looks `Same` rcc robot dependencies --space user + +# you can even overwrite conda.yaml using exact dependencies found from that +# dependencies.yaml, using '--bind' option +rcc robot dependencies --space user --bind ``` ## How pass arguments to robot from CLI? diff --git a/robot/robot.go b/robot/robot.go index c28bedf8..57c8b186 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -231,7 +231,10 @@ func (it *robot) IdealCondaYaml() (string, bool) { if err != nil { return "", false } - ideal, _ := condaEnv.FromDependencies(dependencies) + ideal, ok := condaEnv.FromDependencies(dependencies) + if !ok { + return "", false + } body, err := ideal.AsYaml() if err != nil { return "", false From b6a2928d327cf612fae92995a509a89436d0a8fa Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Fri, 18 Jun 2021 10:43:36 +0300 Subject: [PATCH 156/516] Adding caching / Holotree doc Fixed filenames in cache docs Fixing styling to standard .md and Github UI --- docs/environment-caching.md | 89 ++++++++ docs/holotree-disk-usage.png | Bin 0 -> 4946 bytes docs/rcc-env-hotels.svg | 390 +++++++++++++++++++++++++++++++++++ 3 files changed, 479 insertions(+) create mode 100644 docs/environment-caching.md create mode 100644 docs/holotree-disk-usage.png create mode 100644 docs/rcc-env-hotels.svg diff --git a/docs/environment-caching.md b/docs/environment-caching.md new file mode 100644 index 00000000..4fd28b3b --- /dev/null +++ b/docs/environment-caching.md @@ -0,0 +1,89 @@ +## What is execution environment isolation and caching? + +Managing and isolating the execution environments is one of the biggest tasks and friction points that RCC is solving. The execution environment needs to have all the pieces in place for the robot to execute and that could mean a full setup Python environment, browsers, custom library packages, etc. In RPA solutions, "Work on my machine" just doesn't cut it. + +![](https://imgs.xkcd.com/comics/python_environment.png) + +*Credit: [xkcd.com](https://xkcd.com/1987/)* + +The list of tools and techniques around just Python environment handling and package management is simply staggering:
+`pyenv, venv, pyenv-virtualenv, pip, pipenv, poetry, conda, ...`.
+Instead, how about just `rcc`? + +RCC creates virtual execution environments that only show up as files and folders on the user machine, basically containing the complexity described above. The target is that neither setting up nor using these environments must either depend on or change anything to the user's environment. + +The [hotel analogy below](/rcc/holotree#a-better-analogy-accommodations) describes the problem and evolution of our solutions. + +## The second evolution of environment management in RCC + +The solution that goes by the working title `Holotree` is a big step up when it comes to efficiency, and it opens up a few significant doors for the future. The name for `Holotree` comes from the analogy to the [Holodeck in Star Trek](https://en.wikipedia.org/wiki/Holodeck). When things changed in the Holodeck, the "reload" only meant that, for example, a chair transformed into a table while the surroundings did not change. + +Holotree is an environment cache that stores each unique file only once. When restoring an execution environment, it moves only the files that need to be moved or removed, reducing the disk usage and file I/O (input/output) actions. Fewer files to move and handle means fewer actions for the virus scanner in the local development setup, and in Robocorp Cloud, reduced CPU time. + +Holotree can fit hundreds of unique environments in a small space. + +![Holotree disk usage](holotree-disk-usage.png) + +*Note: A single execution environment can span from 200MB to >1GB* + +There are other significant changes and improvements in Holotree regarding [relocation]() and [file locking](https://en.wikipedia.org/wiki/File_locking). + +The implementation is already available in [RCC v10](https://github.com/robocorp/rcc/blob/master/docs/changelog.md), and we are rolling out it to our developer tools, applications and Cloud during Q3 2021. + +### Relocation and file locking + +Some binary files have the absolute path written into them when set up, which means the resulting file will only work in the same file structure where it was created. + +To move or [relocate]() the environment, one must either create the same file structure on the target machine or edit the binaries to contain the correct file paths. RCC with Holotree records the environment to the cache (a.k.a Hololib) with the original execution base folder for this problem. Thus, the execution will happen in the same structure but with a mechanism that enables relocation inside that base folder. This avoids file locks and isolates the executions. The mechanism has to do with hashing the execution folder name in the base folder. + +[File locking](https://en.wikipedia.org/wiki/File_locking) affects mainly Windows but can affect macOS and Linux, too. Trying to change a file simultaneously in more than one process is usually harmful and requires individual files for different executors for the execution environments. + +For example, when using Robocorp Lab and VS Code extensions on the same machine, it would be nice if the environment files do not get locked. + +For this reason, RCC with Holotree provides a `space` -context so that each execution or client can choose a space where the execution happens. Furthermore, with the partial relocation support described above, the client's space is only made for and used by the clients' discretion, avoiding file collisions and giving control over disk space usage and file I/O to the client applications. + +For example, a normal use case for Robocorp Assistant is that the end-user installs and uses five to ten assistants, and those change pretty rarely. Still, the execution should start pretty much immediately. + +In this case, we would use RCC with Holotree to create a space for each assistant. This way, each assistant has the environment files ready to go with the cost of disk usage. + +On the Workforce side, we could use just a single space as we cannot expect the execution to be similar, but they still share many files. For example, consecutive executions probably have well-over 90% of Python files remain the same, etc. + +## A better analogy: accommodations + +The evolution goes something like this: + +"I invited you to my home." > "Welcome to a hotel built out of ship containers." > "Welcome to an actual Hotel." + +The evolution of these steps is expanded in the following chapters. + +![RCC Environment Hotels](rcc-env-hotels.svg) + +### "I invited you to my home." + +We start with a typical bed-and-breakfast where you set up your own house with kitchen, bathroom, bedroom with fridges, pieces of furniture, etc. Then, depending on the guest, your house is nice and clean after the visit, or the guests trash the place and hide a fish behind the radiator to surprise you a couple of days later. 🐟 + +In the robot dimension, this equates to running pip installs, setting up the correct version of Python and all the dependencies on the machine, executing the robot, and cleaning up all the files and changes made by the execution. A week later, you need to clear the entire pip cache due to some weird collision with another execution. + +### "Welcome to a hotel built out of ship containers." + +As engineers, we try to break a solution down into smaller pieces to mitigate the effects and reduce variables. + +So, after the first guest trashed our house, we change our business model to withstand the occasional rock star or two. 😅 + +We buy shipping containers and deck them out with kitchens, bathrooms, and bedrooms. When we get a visitor, we ask what they want and see if we have a shipping container meeting the requirements or if we need to build a new one. In either case, we get a container for the guest and blob it in our "hotel". Now it does not matter how the guest behaves; after the visit, we can remove the container's content, and the next guest gets a new one. And more importantly, our own house is not damaged. + +In the robot dimension, this is RCC's first environment cache implementation. It uses disk space to provide isolated environments, and if it detects any "pollution" in the environment, it replaces the entire content of the execution folder. The cache always stores the unique pristine version to avoid building from scratch every time. Effective, but not efficient. + +### "Welcome to an actual Hotel." + +After running our "Shipping container Hotel" for a while, we notice we are spending a whole lot of resources to replace the entire container even when the guest only ate a single chocolate bar from the mini-fridge. Also, people asking for different sizes of beds is driving you crazy! + +How about running a regular hotel with some out-of-this-world properties? We still isolate guests to their rooms and protect ourselves from the rock stars, but instead of replacing the entire room every time something is broken or out of place, we send out a janitor and a cleanup crew. + +We also notice that the pristine models of the containers take up a lot of space and have the same furniture. Hence, we decide to store the furniture models as single pieces in one warehouse together with the blueprints of the different room models we have. We also enable the guest to ask for the cleanup crew when making a more extended visit. + +In the robot dimension, this means we have each file once on the Hololib side with the blueprints of the environments we have encountered. + +Each hotel room in the analogy is a Holotree space, and we can load in different content to each space and keep them independent and free from file locking. + +> When we encounter a new environment, we still need to use conda and pip stacks to build the new setup. Environment building will take anything between one to five minutes, depending on the disk and network speeds of the building machine. diff --git a/docs/holotree-disk-usage.png b/docs/holotree-disk-usage.png new file mode 100644 index 0000000000000000000000000000000000000000..0e95e846566202b21bfe8825c82520c7171d71b3 GIT binary patch literal 4946 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 38178fb54c1eeea13aaf1ee7c28c328f0dd1e20a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 28 Jun 2021 13:29:10 +0300 Subject: [PATCH 157/516] RCC-130: environment freeze files (v10.3.0) - creating environment freeze YAML file into output directory on every run --- common/version.go | 2 +- conda/condayaml.go | 45 ++++++++++++++++++++++++++++++++++++++----- docs/changelog.md | 4 ++++ operations/running.go | 20 +++++++++++++++++++ robot/robot.go | 6 ++++++ 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/common/version.go b/common/version.go index 30d73e78..d8e1fc10 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.2.4` + Version = `v10.3.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 85c528ff..db13b012 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -134,13 +134,48 @@ func SummonEnvironment(filename string) *Environment { } } +func (it *Environment) FreezeDependencies(fixed dependencies) *Environment { + result := &Environment{ + Name: it.Name, + Prefix: it.Prefix, + Channels: it.Channels, + Conda: []*Dependency{}, + Pip: []*Dependency{}, + PostInstall: it.PostInstall, + } + for _, dependency := range fixed { + if dependency.Origin == "pypi" { + continue + } + result.Conda = append(result.Conda, &Dependency{ + Original: fmt.Sprintf("%s=%s", dependency.Name, dependency.Version), + Name: dependency.Name, + Qualifier: "=", + Versions: dependency.Version, + }) + } + for _, dependency := range fixed { + if dependency.Origin != "pypi" { + continue + } + result.Pip = append(result.Pip, &Dependency{ + Original: fmt.Sprintf("%s==%s", dependency.Name, dependency.Version), + Name: dependency.Name, + Qualifier: "==", + Versions: dependency.Version, + }) + } + return result +} + func (it *Environment) FromDependencies(fixed dependencies) (*Environment, bool) { result := &Environment{ - Name: it.Name, - Prefix: it.Prefix, - Channels: it.Channels, - Conda: []*Dependency{}, - Pip: []*Dependency{}, + Name: it.Name, + Prefix: it.Prefix, + Channels: it.Channels, + Conda: []*Dependency{}, + Pip: []*Dependency{}, + PostInstall: it.PostInstall, } same := true for _, dependency := range it.Conda { diff --git a/docs/changelog.md b/docs/changelog.md index 0a33fee5..596d6032 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.3.0 (date: 28.6.2021) + +- creating environment freeze YAML file into output directory on every run + ## v10.2.4 (date: 24.6.2021) - added `--bind` option to copy exact dependencies from `dependencies.yaml` diff --git a/operations/running.go b/operations/running.go index e4c2975f..c6b74c42 100644 --- a/operations/running.go +++ b/operations/running.go @@ -29,6 +29,25 @@ type RunFlags struct { NoPipFreeze bool } +func FreezeEnvironmentListing(label string, config robot.Robot) { + goldenfile := conda.GoldenMasterFilename(label) + listing := conda.LoadWantedDependencies(goldenfile) + if len(listing) == 0 { + common.Log("No dependencies found at %q", goldenfile) + return + } + env, err := conda.ReadCondaYaml(config.CondaConfigFile()) + if err != nil { + common.Log("Could not read %q, reason: %v", config.CondaConfigFile(), err) + return + } + frozen := env.FreezeDependencies(listing) + err = frozen.SaveAs(config.FreezeFilename()) + if err != nil { + common.Log("Could not save %q, reason: %v", config.FreezeFilename(), err) + } +} + func ExecutionEnvironmentListing(wantedfile, label string, searchPath pathlib.PathParts, directory, outputDir string, environment []string) bool { common.Timeline("execution environment listing") defer common.Log("--") @@ -222,6 +241,7 @@ func ExecuteTask(flags *RunFlags, template []string, config robot.Robot, todo ro wantedfile, _ := config.DependenciesFile() ExecutionEnvironmentListing(wantedfile, label, searchPath, directory, outputDir, environment) } + FreezeEnvironmentListing(label, config) common.Debug("about to run command - %v", task) if common.NoOutputCapture { _, err = shell.New(environment, directory, task...).Execute(interactive) diff --git a/robot/robot.go b/robot/robot.go index 57c8b186..d8d709fb 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "sort" "strings" @@ -36,6 +37,7 @@ type Robot interface { WorkingDirectory() string ArtifactDirectory() string + FreezeFilename() string Paths() pathlib.PathParts PythonPaths() pathlib.PathParts SearchPath(location string) pathlib.PathParts @@ -351,6 +353,10 @@ func (it *robot) WorkingDirectory() string { return it.Root } +func (it *robot) FreezeFilename() string { + return filepath.Join(it.ArtifactDirectory(), fmt.Sprintf("environment_%s_%s_freeze.yaml", runtime.GOOS, runtime.GOARCH)) +} + func (it *robot) ArtifactDirectory() string { return filepath.Join(it.Root, it.Artifacts) } From efffd77a90ef34a829fb8a0abd6f6c6458e075bf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 29 Jun 2021 12:06:25 +0300 Subject: [PATCH 158/516] RCC-130: environment freeze files (v10.3.1) - cleaning up `rcc robot dependencies` and related code now that freeze is actually implemented - changed `--copy` to `--export` since it better describes the action - removed `--bind` because copying freeze file from run is better way - removed "ideal" conda.yaml printout, since runs now create artifact on every run in new envrionments - removed those robot diagnostics that are misguiding now when dependencies are frozen - updated rpaframework to version 10.3.0 in templates - updated robot tests for rcc --- cmd/robotdependencies.go | 29 +-------- common/version.go | 2 +- conda/condayaml.go | 20 +++--- conda/dependencies.go | 1 - conda/workflows.go | 2 +- docs/changelog.md | 13 ++++ docs/recipes.md | 8 +-- robot/robot.go | 25 -------- robot_tests/bug_reports.robot | 1 - robot_tests/development_process.robot | 2 - robot_tests/exitcodes.robot | 88 +++++++++++---------------- robot_tests/export_holozip.robot | 41 ++++++------- robot_tests/fullrun.robot | 76 ++++++++++++++--------- robot_tests/holotree.robot | 18 +++--- robot_tests/resources.robot | 6 +- templates/extended/conda.yaml | 2 +- templates/python/conda.yaml | 2 +- templates/standard/conda.yaml | 2 +- 18 files changed, 141 insertions(+), 197 deletions(-) diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index a0b44ae4..cdffda3a 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -1,8 +1,6 @@ package cmd import ( - "os" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/operations" @@ -14,8 +12,7 @@ import ( ) var ( - copyDependenciesFlag bool - bindDependenciesFlag bool + exportDependenciesFlag bool ) func doShowDependencies(config robot.Robot, label string) { @@ -36,21 +33,6 @@ func doCopyDependencies(config robot.Robot, label string) { pretty.Guard(err == nil, 2, "Copy %q -> %q failed, reason: %v", source, target, err) } -func doBindDependencies(config robot.Robot, label string) { - ideal, ok := config.IdealCondaYaml() - pretty.Guard(ok, 4, "Could not determine ideal conda.yaml for binding. Sorry.") - filename := config.CondaConfigFile() - err := os.WriteFile(filename, []byte(ideal), 0644) - pretty.Guard(err == nil, 5, "Could not write file %q, reason: %v", filename, err) - common.Log("%sOverwrote %q with ideal conda.yaml content.%s", pretty.Yellow, filename, pretty.Reset) -} - -func doShowIdeal(config robot.Robot, label string) { - ideal, ok := config.IdealCondaYaml() - pretty.Guard(ok, 6, "Could not determine ideal conda.yaml. Sorry.") - common.Log("Ideal conda.yaml based on 'dependencies.yaml' would be:\n%s", ideal) -} - var robotDependenciesCmd = &cobra.Command{ Use: "dependencies", Short: "View wanted vs. available dependencies of robot execution environment.", @@ -62,24 +44,19 @@ var robotDependenciesCmd = &cobra.Command{ } simple, config, _, label := operations.LoadAnyTaskEnvironment(robotFile, forceFlag) pretty.Guard(!simple, 1, "Cannot view dependencies of simple robots.") - if copyDependenciesFlag { + if exportDependenciesFlag { common.Log("--") doCopyDependencies(config, label) } common.Log("--") doShowDependencies(config, label) - if bindDependenciesFlag { - doBindDependencies(config, label) - } - doShowIdeal(config, label) pretty.Ok() }, } func init() { robotCmd.AddCommand(robotDependenciesCmd) - robotDependenciesCmd.Flags().BoolVarP(©DependenciesFlag, "copy", "c", false, "Copy golden-ee.yaml from environment as wanted dependencies.yaml, overwriting previous if exists.") - robotDependenciesCmd.Flags().BoolVarP(&bindDependenciesFlag, "bind", "b", false, "Bind (overwrite) conda.yaml dependencies from 'dependencies.yaml'.") + robotDependenciesCmd.Flags().BoolVarP(&exportDependenciesFlag, "export", "e", false, "Export execution environment description into robot dependencies.yaml, overwriting previous if exists.") robotDependenciesCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Forced environment update.") robotDependenciesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") robotDependenciesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Space to use for execution environment dependencies.") diff --git a/common/version.go b/common/version.go index d8e1fc10..33d35565 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.3.0` + Version = `v10.3.1` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index db13b012..c7734439 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -143,10 +143,15 @@ func (it *Environment) FreezeDependencies(fixed dependencies) *Environment { Pip: []*Dependency{}, PostInstall: it.PostInstall, } + used := make(map[string]bool) for _, dependency := range fixed { if dependency.Origin == "pypi" { continue } + if used[dependency.Name] { + continue + } + used[dependency.Name] = true result.Conda = append(result.Conda, &Dependency{ Original: fmt.Sprintf("%s=%s", dependency.Name, dependency.Version), Name: dependency.Name, @@ -158,6 +163,10 @@ func (it *Environment) FreezeDependencies(fixed dependencies) *Environment { if dependency.Origin != "pypi" { continue } + if used[dependency.Name] { + continue + } + used[dependency.Name] = true result.Pip = append(result.Pip, &Dependency{ Original: fmt.Sprintf("%s==%s", dependency.Name, dependency.Version), Name: dependency.Name, @@ -485,7 +494,6 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b diagnose.Ok("Channels in conda.yaml are ok.") } ok = true - condaCount := len(it.Conda) for _, dependency := range it.Conda { presentation := dependency.Representation() if packages[presentation] { @@ -507,11 +515,6 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b diagnose.Ok("Conda dependencies in conda.yaml are ok.") } ok = true - pipCount := len(it.Pip) - if pipCount > 0 { - diagnose.Warning("", "There is %d pip dependencies. Please, prefer using conda dependencies over pip dependencies.", pipCount) - ok = false - } for _, dependency := range it.Pip { presentation := dependency.Representation() if packages[presentation] { @@ -532,11 +535,6 @@ func (it *Environment) Diagnostics(target *common.DiagnosticStatus, production b if ok { diagnose.Ok("Pip dependencies in conda.yaml are ok.") } - totalCount := condaCount + pipCount - if totalCount > 10 { - diagnose.Warning("", "There are more than 10 dependencies in conda.yaml [conda: %d, pip: %d]. This might cause problems for dependency resolvers.", condaCount, pipCount) - diagnose.Warning("", "Too many dependencies might also indicate lack of focus, doing too much, doing it wrong, or missing cleanup step in development process.") - } if floating { diagnose.Warning("", "Floating dependencies in Robocorp Cloud containers will be slow, because floating environments cannot be cached.") } diff --git a/conda/dependencies.go b/conda/dependencies.go index 06347037..03749b49 100644 --- a/conda/dependencies.go +++ b/conda/dependencies.go @@ -155,7 +155,6 @@ func SideBySideViewOfDependencies(goldenfile, wantedfile string) (err error) { tabbed.Write([]byte("Wanted\tVersion\tOrigin\t|\tNo.\t|\tAvailable\tVersion\tOrigin\t|\tStatus\n")) tabbed.Write([]byte("------\t-------\t------\t+\t---\t+\t---------\t-------\t------\t+\t------\n")) for at, key := range keyset { - //left, right, status := "n/a\tn/a\tn/a\t", "\tn/a\tn/a\tn/a", unknown left, right, status := "-\t-\t-\t", "\t-\t-\t-", unknown sides := diffmap[key] if sides[0] < 0 || sides[1] < 0 { diff --git a/conda/workflows.go b/conda/workflows.go index f8e362d0..284a04e3 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -425,7 +425,7 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { before := make(map[string]string) beforeHash, beforeErr := DigestFor(templateFolder, before) DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after, false) - } else { + } else if pathlib.IsDir(templateFolder) { common.Log("WARNING! Template is NOT pristine: %q", templateFolder) } common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) diff --git a/docs/changelog.md b/docs/changelog.md index 596d6032..228d8e30 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,18 @@ # rcc change log +## v10.3.1 (date: 29.6.2021) + +- cleaning up `rcc robot dependencies` and related code now that freeze is + actually implemented +- changed `--copy` to `--export` since it better describes the action +- removed `--bind` because copying freeze file from run is better way +- removed "ideal" conda.yaml printout, since runs now create artifact + on every run in new envrionments +- removed those robot diagnostics that are misguiding now when dependencies + are frozen +- updated rpaframework to version 10.3.0 in templates +- updated robot tests for rcc + ## v10.3.0 (date: 28.6.2021) - creating environment freeze YAML file into output directory on every run diff --git a/docs/recipes.md b/docs/recipes.md index a2dc634e..e14a558f 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -35,15 +35,11 @@ robot runs. # first list dependencies from execution environment rcc robot dependencies --space user -# if everything looks good, copy it as wanted dependencies.yaml -rcc robot dependencies --space user --copy +# if everything looks good, export it as wanted dependencies.yaml +rcc robot dependencies --space user --export # and verify that everything looks `Same` rcc robot dependencies --space user - -# you can even overwrite conda.yaml using exact dependencies found from that -# dependencies.yaml, using '--bind' option -rcc robot dependencies --space user --bind ``` ## How pass arguments to robot from CLI? diff --git a/robot/robot.go b/robot/robot.go index d8d709fb..c3c75bbc 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -33,7 +33,6 @@ type Robot interface { Validate() (bool, error) Diagnostics(*common.DiagnosticStatus, bool) DependenciesFile() (string, bool) - IdealCondaYaml() (string, bool) WorkingDirectory() string ArtifactDirectory() string @@ -220,30 +219,6 @@ func (it *robot) DependenciesFile() (string, bool) { return filename, pathlib.IsFile(filename) } -func (it *robot) IdealCondaYaml() (string, bool) { - wanted, ok := it.DependenciesFile() - if !ok { - return "", false - } - dependencies := conda.LoadWantedDependencies(wanted) - if len(dependencies) == 0 { - return "", false - } - condaEnv, err := conda.ReadCondaYaml(it.CondaConfigFile()) - if err != nil { - return "", false - } - ideal, ok := condaEnv.FromDependencies(dependencies) - if !ok { - return "", false - } - body, err := ideal.AsYaml() - if err != nil { - return "", false - } - return body, true -} - func (it *robot) VerifyCondaDependencies() bool { wanted, ok := it.DependenciesFile() if !ok { diff --git a/robot_tests/bug_reports.robot b/robot_tests/bug_reports.robot index 4ca5ea68..c24cdc95 100644 --- a/robot_tests/bug_reports.robot +++ b/robot_tests/bug_reports.robot @@ -7,7 +7,6 @@ Github issue 7 about initial call with do-not-track [Setup] Remove config tmp/bug_7.yaml Wont Exist tmp/bug_7.yaml - Goal First time calling rcc configure identity to disable tracking Step build/rcc configure identity --controller citests --do-not-track --config tmp/bug_7.yaml Must Have anonymous health tracking is: disabled diff --git a/robot_tests/development_process.robot b/robot_tests/development_process.robot index 7e846831..0c49c60f 100644 --- a/robot_tests/development_process.robot +++ b/robot_tests/development_process.robot @@ -4,8 +4,6 @@ Resource resources.robot *** Test cases *** Has required changes in commit based on development process. - - Goal See git changes for required files have been changed Step git show --stat Must Have docs/changelog.md Must Have common/version.go diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index 0d083fb4..3f237db0 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -4,60 +4,40 @@ Test template Verify exitcodes *** Test cases *** EXITCODE COMMAND -General failure of rcc command 1 build/rcc crapiti -h --controller citests - -General output for rcc command 0 build/rcc --controller citests - -Help for rcc command 0 build/rcc -h - -Help for rcc assistant subcommand 0 build/rcc assistant -h --controller citests -Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citests -Help for rcc configure subcommand 0 build/rcc configure -h --controller citests -Help for rcc env subcommand 0 build/rcc env -h --controller citests -Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests -Help for rcc help subcommand 0 build/rcc help -h --controller citests -Help for rcc man subcommand 0 build/rcc man -h --controller citests -Help for rcc internal subcommand 0 build/rcc internal -h --controller citests -Help for rcc robot subcommand 0 build/rcc robot -h --controller citests -Help for rcc task subcommand 0 build/rcc task -h --controller citests -Help for rcc version subcommand 0 build/rcc version -h --controller citests - -Help for rcc assistant list 0 build/rcc assistant list -h --controller citests -Help for rcc assistant run 0 build/rcc assistant run -h --controller citests - -Help for rcc cloud authorize 0 build/rcc cloud authorize -h --controller citests -Help for rcc cloud download 0 build/rcc cloud download -h --controller citests -Help for rcc cloud new 0 build/rcc cloud new -h --controller citests -Help for rcc cloud pull 0 build/rcc cloud pull -h --controller citests -Help for rcc cloud push 0 build/rcc cloud push -h --controller citests -Help for rcc cloud upload 0 build/rcc cloud upload -h --controller citests -Help for rcc cloud userinfo 0 build/rcc cloud userinfo -h --controller citests -Help for rcc cloud workspace 0 build/rcc cloud workspace -h --controller citests - -Help for rcc configure credentials 0 build/rcc configure credentials -h --controller citests - -Help for rcc env delete 0 build/rcc env delete -h --controller citests -Help for rcc env list 0 build/rcc env list -h --controller citests -Help for rcc env new 0 build/rcc env new -h --controller citests -Help for rcc env variables 0 build/rcc env variables -h --controller citests - -Help for rcc configure identity 0 build/rcc configure identity -h --controller citests -Help for rcc configure diagnostics 0 build/rcc configure diagnostics -h --controller citests - -Help for rcc feedback metric 0 build/rcc feedback metric -h --controller citests - -Help for rcc man license 0 build/rcc man license -h --controller citests - -Help for rcc robot fix 0 build/rcc robot fix -h --controller citests -Help for rcc robot initialize 0 build/rcc robot initialize -h --controller citests -Help for rcc robot libs 0 build/rcc robot libs -h --controller citests -Help for rcc robot list 0 build/rcc robot list -h --controller citests -Help for rcc robot unwrap 0 build/rcc robot unwrap -h --controller citests -Help for rcc robot wrap 0 build/rcc robot wrap -h --controller citests - -Help for rcc task run 0 build/rcc task run -h --controller citests -Help for rcc task shell 0 build/rcc task shell -h --controller citests -Help for rcc task testrun 0 build/rcc task testrun -h --controller citests +General failure of rcc command 1 build/rcc crapiti -h --controller citests + +General output for rcc command 0 build/rcc --controller citests + +Help for rcc command 0 build/rcc -h + +Help for rcc assistant subcommand 0 build/rcc assistant -h --controller citests +Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citests +Help for rcc community subcommand 0 build/rcc community -h --controller citests +Help for rcc configure subcommand 0 build/rcc configure -h --controller citests +Help for rcc create subcommand 0 build/rcc create -h --controller citests +Help for rcc env subcommand 0 build/rcc env -h --controller citests +Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests +Help for rcc holotree subcommand 0 build/rcc holotree -h --controller citests +Help for rcc help subcommand 0 build/rcc help -h --controller citests +Help for rcc interactive subcommand 0 build/rcc interactive -h --controller citests +Help for rcc internal subcommand 0 build/rcc internal -h --controller citests +Help for rcc man subcommand 0 build/rcc man -h --controller citests +Help for rcc pull subcommand 0 build/rcc pull -h --controller citests +Help for rcc robot subcommand 0 build/rcc robot -h --controller citests +Help for rcc run subcommand 0 build/rcc run -h --controller citests +Help for rcc task subcommand 0 build/rcc task -h --controller citests +Help for rcc tutorial subcommand 0 build/rcc tutorial -h --controller citests +Help for rcc version subcommand 0 build/rcc version -h --controller citests + +Run rcc config settings 0 build/rcc config settings --controller citests +Run rcc docs changelog 0 build/rcc docs changelog --controller citests +Run rcc docs license 0 build/rcc docs license --controller citests +Run rcc docs recipes 0 build/rcc docs recipes --controller citests +Run rcc docs tutorial 0 build/rcc docs tutorial --controller citests +Run rcc environment list 0 build/rcc environment list --controller citests +Run rcc holotree list 0 build/rcc holotree list --controller citests +Run rcc tutorial 0 build/rcc tutorial --controller citests +Run rcc version 0 build/rcc version --controller citests *** Keywords *** diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 34476eff..5b02b484 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -10,6 +10,7 @@ Export setup Remove Directory tmp/developer True Remove Directory tmp/guest True Remove Directory tmp/standalone True + Set Environment Variable ROBOCORP_HOME tmp/developer Export teardown Set Environment Variable ROBOCORP_HOME tmp/robocorp @@ -19,15 +20,12 @@ Export teardown *** Test cases *** -Workflow with hololib.zip export - Set Environment Variable ROBOCORP_HOME tmp/developer - - Goal Create extended robot into tmp/standalone folder using force. +Goal: Create extended robot into tmp/standalone folder using force. Step build/rcc robot init --controller citests -t extended -d tmp/standalone -f Use STDERR Must Have OK. - Goal Create environment for standalone robot +Goal: Create environment for standalone robot Step build/rcc ht vars -s author --controller citests -r tmp/standalone/robot.yaml Must Have RCC_ENVIRONMENT_HASH= Must Have RCC_INSTALLATION_ID= @@ -37,75 +35,74 @@ Workflow with hololib.zip export Must Have Progress: 4/6 Must Have Progress: 6/6 - Goal Must have author space visible +Goal: Must have author space visible Step build/rcc ht ls Use STDERR Must Have 4e67cd8d4_fcb4b859 Must Have rcc.citests Must Have author - Must Have f130d7d72d4d4663 + Must Have 2e3ef3ffef58c9ec Wont Have guest - Goal Show exportable environment list +Goal: Show exportable environment list Step build/rcc ht export Use STDERR Must Have Selectable catalogs - Must Have - f130d7d72d4d4663 + Must Have - 2e3ef3ffef58c9ec Must Have OK. - Goal Export environment for standalone robot - Step build/rcc ht export -z tmp/standalone/hololib.zip f130d7d72d4d4663 +Goal: Export environment for standalone robot + Step build/rcc ht export -z tmp/standalone/hololib.zip 2e3ef3ffef58c9ec Use STDERR Wont Have Selectable catalogs Must Have OK. - Goal Wrap the robot +Goal: Wrap the robot Step build/rcc robot wrap -z tmp/full.zip -d tmp/standalone/ Use STDERR Must Have OK. - Goal See contents of that robot +Goal: See contents of that robot Step unzip -v tmp/full.zip Must Have robot.yaml Must Have conda.yaml Must Have hololib.zip - Goal Can delete author space +Goal: Can delete author space Step build/rcc ht delete 4e67cd8d4_fcb4b859 Step build/rcc ht ls Use STDERR Wont Have 4e67cd8d4_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have f130d7d72d4d4663 + Wont Have 2e3ef3ffef58c9ec Wont Have guest +Goal: Can run as guest Set Environment Variable ROBOCORP_HOME tmp/guest - - Goal Can run as guest Step build/rcc task run --controller citests -s guest -r tmp/standalone/robot.yaml -t 'run example task' Use STDERR Wont Have Downloading micromamba Must Have OK. - Goal No spaces created under guest +Goal: No spaces created under guest + Set Environment Variable ROBOCORP_HOME tmp/guest Step build/rcc ht ls Use STDERR Wont Have 4e67cd8d4_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have f130d7d72d4d4663 + Wont Have 2e3ef3ffef58c9ec Wont Have 4e67cd8d4_559e19be Wont Have guest +Goal: Space created under author for guest Set Environment Variable ROBOCORP_HOME tmp/developer - - Goal Space created under author for guest Step build/rcc ht ls Use STDERR Wont Have 4e67cd8d4_fcb4b859 Wont Have author Must Have rcc.citests - Must Have f130d7d72d4d4663 + Must Have 2e3ef3ffef58c9ec Must Have 4e67cd8d4_aacf1552 Must Have guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 1a4a3201..cd31744c 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -5,13 +5,11 @@ Resource resources.robot *** Test cases *** -Using and running template example with shell file - - Goal Show rcc version information. +Goal: Show rcc version information. Step build/rcc version --controller citests Must Have v10. - Goal Show rcc license information. +Goal: Show rcc license information. Step build/rcc man license --controller citests Must Have Apache License Must Have Version 2.0 @@ -19,37 +17,37 @@ Using and running template example with shell file Must Have Copyright 2020 Robocorp Technologies, Inc. Wont Have EULA - Goal Telemetry tracking enabled by default. +Goal: Telemetry tracking enabled by default. Step build/rcc configure identity --controller citests Must Have anonymous health tracking is: enabled Must Exist %{ROBOCORP_HOME}/rcc.yaml Wont Exist %{ROBOCORP_HOME}/rcccache.yaml - Goal Send telemetry data to cloud. +Goal: Send telemetry data to cloud. Step build/rcc feedback metric --controller citests -t test -n rcc.test -v robot.fullrun Use STDERR Must Have OK - Goal Telemetry tracking can be disabled. +Goal: Telemetry tracking can be disabled. Step build/rcc configure identity --controller citests --do-not-track Must Have anonymous health tracking is: disabled - Goal Show listing of rcc commands. +Goal: Show listing of rcc commands. Step build/rcc --controller citests Use STDERR Must Have rcc is environment manager Wont Have missing - Goal Show toplevel help for rcc. +Goal: Show toplevel help for rcc. Step build/rcc -h Must Have Available Commands: - Goal Show config help for rcc. +Goal: Show config help for rcc. Step build/rcc config -h --controller citests Must Have Available Commands: Must Have credentials - Goal List available robot templates. +Goal: List available robot templates. Step build/rcc robot init -l --controller citests Must Have extended Must Have python @@ -57,38 +55,59 @@ Using and running template example with shell file Use STDERR Must Have OK. - Goal Initialize new standard robot into tmp/fluffy folder using force. +Goal: Initialize new standard robot into tmp/fluffy folder using force. Step build/rcc robot init --controller citests -t extended -d tmp/fluffy -f Use STDERR Must Have OK. - Goal There should now be fluffy in robot listing +Goal: There should now be fluffy in robot listing Step build/rcc robot list --controller citests -j Must Be Json Response Must Have fluffy Must Have "robot" - Goal Fail to initialize new standard robot into tmp/fluffy without force. +Goal: Fail to initialize new standard robot into tmp/fluffy without force. Step build/rcc robot init --controller citests -t extended -d tmp/fluffy 2 Use STDERR Must Have Error: Directory Must Have fluffy is not empty + Wont Exist tmp/fluffy/output/environment_*_freeze.yaml - Goal Run task in place. - Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml +Goal: Run task in place in debug mode and with timeline. + Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have Progress: 0/6 Must Have Progress: 1/6 Must Have Progress: 6/6 Must Have rpaframework + Must Have PID # + Must Have [N] + Must Have [D] + Wont Have [T] + Wont Have Running against old environment + Wont Have WARNING + Wont Have NOT pristine + Must Have Golden EE file at: + Must Have Installation plan is: + Must Have Command line is: [ + Must Have rcc timeline + Must Have robot execution starts (simple=false). + Must Have robot execution done. + Must Have Now. + Must Have Wanted + Must Have Available + Must Have Version + Must Have Origin + Must Have Status Must Have OK. + Must Exist tmp/fluffy/output/environment_*_freeze.yaml Must Exist %{ROBOCORP_HOME}/base/ Must Exist %{ROBOCORP_HOME}/live/ Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ - Goal Run task in clean temporary directory. +Goal: Run task in clean temporary directory. Step build/rcc task testrun --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml Must Have 1 task, 1 passed, 0 failed Use STDERR @@ -102,21 +121,21 @@ Using and running template example with shell file Must Have Progress: 6/6 Must Have OK. - Goal Merge two different conda.yaml files with conflict fails +Goal: Merge two different conda.yaml files with conflict fails Step build/rcc env new --controller citests conda/testdata/conda.yaml conda/testdata/other.yaml 1 Use STDERR Must Have robotframework=3.1 vs. robotframework=3.2 - Goal Merge two different conda.yaml files without conflict passes +Goal: Merge two different conda.yaml files without conflict passes Step build/rcc env new --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent Must Have 0cc761cfb9692a36 - Goal Can list environments as JSON +Goal: Can list environments as JSON Step build/rcc env list --controller citests --json Must Have 0cc761cfb9692a36 Must Be Json Response - Goal See variables from specific environment without robot.yaml knowledge +Goal: See variables from specific environment without robot.yaml knowledge Step build/rcc env variables --controller citests conda/testdata/conda.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -138,7 +157,7 @@ Using and running template example with shell file Wont Have ROBOT_ARTIFACTS= Must Have f0a9e281269b31ea - Goal See variables from specific environment with robot.yaml but without task +Goal: See variables from specific environment with robot.yaml but without task Step build/rcc env variables --controller citests -r tmp/fluffy/robot.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -152,19 +171,18 @@ Using and running template example with shell file Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= - Must Have RCC_ENVIRONMENT_HASH= + Must Have RCC_ENVIRONMENT_HASH=199e494e4c733ef3 Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= Must Have PYTHONPATH= Must Have ROBOT_ROOT= Must Have ROBOT_ARTIFACTS= - Must Have 7b3eba72202108b9 - Goal See variables from specific environment without robot.yaml knowledge in JSON form +Goal: See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml Must Be Json Response - Goal See variables from specific environment with robot.yaml knowledge +Goal: See variables from specific environment with robot.yaml knowledge Step build/rcc env variables --task "Run Example task" --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -190,15 +208,15 @@ Using and running template example with shell file Wont Have RC_API_WORKITEM_TOKEN= Wont Have RC_WORKSPACE_ID= - Goal See variables from specific environment with robot.yaml knowledge in JSON form +Goal: See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc env variables --task "Run Example task" --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Be Json Response - Goal See diagnostics as valid JSON form +Goal: See diagnostics as valid JSON form Step build/rcc configure diagnostics --json Must Be Json Response - Goal Simulate issue report sending with dryrun +Goal: Simulate issue report sending with dryrun Step build/rcc feedback issue --dryrun --report robot_tests/report.json --attachments robot_tests/conda.yaml Must Have "report": Must Have "zipfile": diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 63c28cfe..adb8f9b2 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -5,14 +5,12 @@ Resource resources.robot *** Test cases *** -Holotree testing flow - - Goal Initialize new standard robot into tmp/holotin folder using force. +Goal: Initialize new standard robot into tmp/holotin folder using force. Step build/rcc robot init --controller citests -t extended -d tmp/holotin -f Use STDERR Must Have OK. - Goal See variables from specific environment without robot.yaml knowledge +Goal: See variables from specific environment without robot.yaml knowledge Step build/rcc holotree variables --space jam --controller citests conda/testdata/conda.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -33,7 +31,7 @@ Holotree testing flow Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= - Goal See variables from specific environment with robot.yaml but without task +Goal: See variables from specific environment with robot.yaml but without task Step build/rcc holotree variables --space jam --controller citests -r tmp/holotin/robot.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -54,11 +52,11 @@ Holotree testing flow Must Have ROBOT_ROOT= Must Have ROBOT_ARTIFACTS= - Goal See variables from specific environment without robot.yaml knowledge in JSON form +Goal: See variables from specific environment without robot.yaml knowledge in JSON form Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml Must Be Json Response - Goal See variables from specific environment with robot.yaml knowledge +Goal: See variables from specific environment with robot.yaml knowledge Step build/rcc holotree variables --space jam --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -87,14 +85,14 @@ Holotree testing flow Wont Have (virtual) Must Have live only - Goal See variables from specific environment with robot.yaml knowledge in JSON form +Goal: See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json Must Be Json Response Use STDERR Wont Have (virtual) Wont Have live only - Goal Liveonly works and uses virtual holotree +Goal: Liveonly works and uses virtual holotree Step build/rcc holotree vars --liveonly --space jam --controller citests robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= @@ -123,7 +121,7 @@ Holotree testing flow Must Have (virtual) Must Have live only - Goal Liveonly works and uses virtual holotree and can give output in JSON form +Goal: Liveonly works and uses virtual holotree and can give output in JSON form Step build/rcc ht vars --liveonly --space jam --controller citests --json robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline Must Be Json Response Use STDERR diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index b280dc45..57f5d065 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -15,7 +15,7 @@ Prepare Local Create Directory tmp/robocorp Set Environment Variable ROBOCORP_HOME tmp/robocorp - Goal Verify micromamba is installed or download and install it. + Comment Verify micromamba is installed or download and install it. Step build/rcc env new robot_tests/conda.yaml Must Exist %{ROBOCORP_HOME}/bin/ Must Exist %{ROBOCORP_HOME}/base/ @@ -23,10 +23,6 @@ Prepare Local Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ -Goal - [Arguments] ${anything} - Comment ${anything} - Step [Arguments] ${command} ${expected}=0 ${code} ${output} ${error}= Run and return code output error ${command} diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index fa5adbb9..a2e9e121 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html + - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index fa5adbb9..a2e9e121 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html + - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index fa5adbb9..a2e9e121 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==9.5.0 # https://rpaframework.org/releasenotes.html + - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html From 8bade141cb0c5c93b153fa9e940f73eda3b72096 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 29 Jun 2021 12:42:28 +0300 Subject: [PATCH 159/516] RCC-130: environment freeze files (v10.3.2) - fix for missing artifact directory on runs --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/running.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 33d35565..3bb17f09 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.3.1` + Version = `v10.3.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 228d8e30..c8997f19 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.3.2 (date: 29.6.2021) + +- fix for missing artifact directory on runs + ## v10.3.1 (date: 29.6.2021) - cleaning up `rcc robot dependencies` and related code now that freeze is diff --git a/operations/running.go b/operations/running.go index c6b74c42..cdce6ea4 100644 --- a/operations/running.go +++ b/operations/running.go @@ -129,6 +129,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. func SelectExecutionModel(runFlags *RunFlags, simple bool, template []string, config robot.Robot, todo robot.Task, label string, interactive bool, extraEnv map[string]string) { common.Timeline("robot execution starts (simple=%v).", simple) defer common.Timeline("robot execution done.") + pathlib.EnsureDirectoryExists(config.ArtifactDirectory()) if simple { ExecuteSimpleTask(runFlags, template, config, todo, interactive, extraEnv) } else { From 35d630ddd2dc28f1b08569b811a1a3b28264450a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 29 Jun 2021 17:48:03 +0300 Subject: [PATCH 160/516] RCC-130: environment freeze files (v10.3.3) - updated tips, tricks, and recipes --- common/version.go | 2 +- docs/changelog.md | 6 ++- docs/recipes.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index 3bb17f09..8a342a2e 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.3.2` + Version = `v10.3.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index c8997f19..eff4a512 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,10 +1,14 @@ # rcc change log +## v10.3.3 (date: 29.6.2021) + +- updated tips, tricks, and recipes + ## v10.3.2 (date: 29.6.2021) - fix for missing artifact directory on runs -## v10.3.1 (date: 29.6.2021) +## v10.3.1 (date: 29.6.2021) broken - cleaning up `rcc robot dependencies` and related code now that freeze is actually implemented diff --git a/docs/recipes.md b/docs/recipes.md index e14a558f..17977c7c 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -42,6 +42,39 @@ rcc robot dependencies --space user --export rcc robot dependencies --space user ``` +## How to freeze dependencies? + +Starting from rcc 10.3.2, there is now possibility to freeze dependencies. +This is how you can experiment with it. + +### Steps + +- have your `conda.yaml` to contain only those dependencies that your robot + needs, either with exact versions or floating ones +- run robot in your target environment at least once, so that environment + there gets created +- from that run's artifact directory, you should find file that has name + something like `environment_xxx_yyy_freeze.yaml` +- copy that file back into your robot, right beside existing `conda.yaml` + file (but do not overwrite it, you need that later) +- edit your `robot.yaml` file to point `condaConfigFile` entry to your + newly created `environment_xxx_yyy_freeze.yaml` file +- repackage your robot and now your environment should stay quite frozen + +### Limitations + +- this is new and experimental feature, and we don't know yet how well it + works in all cases (but we love to get feedback) +- currently this freezing limits where robot can be run, since dependencies + on different operating systems and architectures differ and freezing cannot + be done in OS and architecture neutral way +- your robot will break, if some specific package is removed from pypi or + conda repositories +- your robot might also break, if someone updates package (and it's dependencies) + without changing its version number +- for better visibility on configuration drift, you should also have + `dependencies.yaml` inside your robot (see other recipe for it) + ## How pass arguments to robot from CLI? Since version 9.15.0, rcc supports passing arguments from CLI to underlying @@ -95,6 +128,98 @@ rcc task script --silent -- pip list rcc task script --interactive -- ipython ``` +## Is rcc limited to Python and Robot Framework? + +Absolutely not! Here is something completely different for you to think about. + +Lets assume, that you are in almost empty Linux machine, and you have to +quickly build new micromamba in that machine. Hey, there is `bash`, `$EDITOR`, +and `curl` here. But there are no compilers, git, or even python installed. + +> Pop quiz, hot shot! Who you gonna call? MacGyver! + +### This is what we are going to do ... + +Here is set of commands we are going to execute in our trusty shell + +```sh +mkdir -p builder/bin +cd builder +$EDITOR robot.yaml +$EDITOR conda.yaml +$EDITOR bin/builder.sh +curl -o rcc https://downloads.robocorp.com/rcc/releases/v10.3.2/linux64/rcc +chmod 755 rcc +./rcc run -s MacGyver +``` + +### Write a robot.yaml + +So, for this to be a robot, we need to write heart of our robot, which is +`robot.yaml` of course. + +```yaml +tasks: + µmamba: + shell: builder.sh +condaConfigFile: conda.yaml +artifactsDir: output +PATH: +- bin +``` + +### Write a conda.yaml + +Next, we need to define what our robot needs to be able to do our mighty task. +This goes into `conda.yaml` file. + +```yaml +channels: +- conda-forge +dependencies: +- git +- gmock +- cli11 +- cmake +- compilers +- cxx-compiler +- pybind11 +- libsolv +- libarchive +- libcurl +- gtest +- nlohmann_json +- cpp-filesystem +- yaml-cpp +- reproc-cpp +- python=3.8 +- pip=20.1 +``` + +### Write a bin/builder.sh + +And finally, what does our robot do. And this time, this goes to our directory +bin, which is on our PATH, and name for this "robot" is actually `builder.sh` +and it is a bash script. + +```sh +#!/bin/bash -ex + +rm -rf target output/micromamba* +git clone https://github.com/mamba-org/mamba.git target +pushd target +version=$(git tag -l --sort='-creatordate' | head -1) +git checkout $version +mkdir -p build +pushd build +cmake .. -DCMAKE_INSTALL_PREFIX=/tmp/mamba -DENABLE_TESTS=ON -DBUILD_EXE=ON -DBUILD_BINDINGS=OFF +make +popd +popd +mkdir -p output +cp target/build/micromamba output/micromamba-$version +``` + ## Where can I find updates for rcc? https://downloads.robocorp.com/rcc/releases/index.html @@ -114,3 +239,10 @@ https://github.com/robocorp/rcc/blob/master/docs/changelog.md ```sh rcc docs changelog ``` + +## Can I see these tips as web page? + +Sure. See following URL. + +https://github.com/robocorp/rcc/blob/master/docs/recipes.md + From dad654583beb141d37f965ac0bb1355ff9213175 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 5 Aug 2021 10:04:09 +0300 Subject: [PATCH 161/516] RCC-189: activation bug fix (v10.4.0) - bug fix: `rcc_activate.sh` were failing, when path to rcc has spaces in it --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 4 ++-- conda/platform_linux_amd64.go | 4 ++-- docs/changelog.md | 4 ++++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 8a342a2e..9904ac87 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.3.3` + Version = `v10.4.0` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 1a2c755b..8250a9a1 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -15,8 +15,8 @@ const ( activateScript = `#!/bin/bash export MAMBA_ROOT_PREFIX={{.Robocorphome}} -eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" -{{.Rcc}} internal env -l after +eval "$('{{.Micromamba}}' shell activate -s bash -p {{.Live}})" +"{{.Rcc}}" internal env -l after ` commandSuffix = ".sh" ) diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index 95ea7a39..cbfd511a 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -15,8 +15,8 @@ const ( activateScript = `#!/bin/bash export MAMBA_ROOT_PREFIX={{.Robocorphome}} -eval "$({{.Micromamba}} shell activate -s bash -p {{.Live}})" -{{.Rcc}} internal env -l after +eval "$('{{.Micromamba}}' shell activate -s bash -p {{.Live}})" +"{{.Rcc}}" internal env -l after ` commandSuffix = ".sh" ) diff --git a/docs/changelog.md b/docs/changelog.md index eff4a512..e43a040b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.4.0 (date: 5.8.2021) + +- bug fix: `rcc_activate.sh` were failing, when path to rcc has spaces in it + ## v10.3.3 (date: 29.6.2021) - updated tips, tricks, and recipes From 3f0f181c0b0e29e116ebfc191dba71148c4e7534 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 5 Aug 2021 10:53:28 +0300 Subject: [PATCH 162/516] RCC-188: update micromamba (v10.4.1) - taking micromamba 0.15.2 into use --- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 4 ++++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 9904ac87..c082a0a2 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.0` + Version = `v10.4.1` ) diff --git a/conda/platform_darwin_amd64.go b/conda/platform_darwin_amd64.go index 8250a9a1..f2371943 100644 --- a/conda/platform_darwin_amd64.go +++ b/conda/platform_darwin_amd64.go @@ -44,7 +44,7 @@ func CondaPaths(prefix string) []string { } func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.14.0/macos64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.15.2/macos64/micromamba") } func IsWindows() bool { diff --git a/conda/platform_linux_amd64.go b/conda/platform_linux_amd64.go index cbfd511a..22766251 100644 --- a/conda/platform_linux_amd64.go +++ b/conda/platform_linux_amd64.go @@ -27,7 +27,7 @@ var ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.14.0/linux64/micromamba") + return settings.Global.DownloadsLink("micromamba/v0.15.2/linux64/micromamba") } func CondaEnvironment() []string { diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index bf9fd373..fb8a76c5 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -28,7 +28,7 @@ const ( ) func MicromambaLink() string { - return settings.Global.DownloadsLink("micromamba/v0.14.0/windows64/micromamba.exe") + return settings.Global.DownloadsLink("micromamba/v0.15.2/windows64/micromamba.exe") } var ( diff --git a/conda/robocorp.go b/conda/robocorp.go index 92e09729..5b8dfd12 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -233,7 +233,7 @@ func HasMicroMamba() bool { return false } version, versionText := asVersion(MicromambaVersion()) - goodEnough := version >= 14000 + goodEnough := version >= 15002 common.Debug("%q version is %q -> %v (good enough: %v)", BinMicromamba(), versionText, version, goodEnough) common.Timeline("µmamba version is %q (at %q).", versionText, BinMicromamba()) return goodEnough diff --git a/docs/changelog.md b/docs/changelog.md index e43a040b..8b6a0c4c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.4.1 (date: 5.8.2021) + +- taking micromamba 0.15.2 into use + ## v10.4.0 (date: 5.8.2021) - bug fix: `rcc_activate.sh` were failing, when path to rcc has spaces in it From 6c524042a8c2076baa495ff7957af5a29dfa85e1 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 5 Aug 2021 16:51:51 +0300 Subject: [PATCH 163/516] BUGFIX: scaling down holotree concurrency (v10.4.2) - bugfix: scaling down holotree concurrency, since at least Mac file limits are hit by current concurrency limit --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index c082a0a2..0aa6173b 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.1` + Version = `v10.4.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 8b6a0c4c..adf3789b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.4.2 (date: 5.8.2021) + +- bugfix: scaling down holotree concurrency, since at least Mac file limits + are hit by current concurrency limit + ## v10.4.1 (date: 5.8.2021) - taking micromamba 0.15.2 into use diff --git a/htfs/commands.go b/htfs/commands.go index a299b047..abbd6a3b 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -33,7 +33,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) - anywork.Scale(200) + anywork.Scale(100) tree, err := New() fail.On(err != nil, "%s", err) From 731b0db1ae23ff3021b2bcb9dd6b9853986f805a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 9 Aug 2021 14:51:02 +0300 Subject: [PATCH 164/516] BUGFIX: removing holotree fs syncs (v10.4.3) - bugfix: trying to fix Mac related slowing by removing file syncs on holotree copy processes --- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/functions.go | 6 ------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index 0aa6173b..a8c844bb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.2` + Version = `v10.4.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index adf3789b..e0a1c526 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.4.3 (date: 9.8.2021) + +- bugfix: trying to fix Mac related slowing by removing file syncs on + holotree copy processes + ## v10.4.2 (date: 5.8.2021) - bugfix: scaling down holotree concurrency, since at least Mac file limits diff --git a/htfs/functions.go b/htfs/functions.go index 0da33e84..74260a18 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -149,7 +149,6 @@ func LiftFile(sourcename, sinkname string) anywork.Work { if err != nil { panic(err) } - sink.Sync() } } @@ -169,7 +168,6 @@ func LiftFlatFile(sourcename, sinkname string) anywork.Work { if err != nil { panic(err) } - sink.Sync() } } @@ -189,7 +187,6 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ if err != nil { panic(err) } - sink.Sync() for _, position := range details.Rewrite { _, err = sink.Seek(position, 0) if err != nil { @@ -200,7 +197,6 @@ func DropFile(library Library, digest, sinkname string, details *File, rewrite [ panic(err) } } - sink.Sync() os.Chmod(sinkname, details.Mode) os.Chtimes(sinkname, motherTime, motherTime) } @@ -222,7 +218,6 @@ func DropFlatFile(sourcename, sinkname string, details *File, rewrite []byte) an if err != nil { panic(err) } - sink.Sync() for _, position := range details.Rewrite { _, err = sink.Seek(position, 0) if err != nil { @@ -233,7 +228,6 @@ func DropFlatFile(sourcename, sinkname string, details *File, rewrite []byte) an panic(err) } } - sink.Sync() os.Chmod(sinkname, details.Mode) os.Chtimes(sinkname, motherTime, motherTime) } From ba420559d42cb81f68de2425c9200e681d850a61 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 9 Aug 2021 16:02:31 +0300 Subject: [PATCH 165/516] BUGFIX: raising inital holotree scaling (v10.4.4) - bugfix: raising initial scaling factor to 16, so that there should always be workers waiting --- anywork/worker.go | 4 ++-- common/version.go | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/anywork/worker.go b/anywork/worker.go index e23dad3c..6ef9fabe 100644 --- a/anywork/worker.go +++ b/anywork/worker.go @@ -61,8 +61,8 @@ func init() { pipeline = make(WorkQueue, 100000) failpipe = make(Failures) errcount = make(Counters) - headcount = 1 - go member(headcount) + headcount = 0 + Scale(16) go watcher(failpipe, errcount) } diff --git a/common/version.go b/common/version.go index a8c844bb..e7850e1a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.3` + Version = `v10.4.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index e0a1c526..35d478d2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.4.4 (date: 9.8.2021) + +- bugfix: raising initial scaling factor to 16, so that there should always + be workers waiting + ## v10.4.3 (date: 9.8.2021) - bugfix: trying to fix Mac related slowing by removing file syncs on From 11ebc264499c0a3c1df6599cbe1238a948d760c4 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 10 Aug 2021 10:07:25 +0300 Subject: [PATCH 166/516] BUGFIX: one more Mac OS fix (v10.4.5) - bugfix: removing one more filesystem sync from holotree (Mac slowdown fix). --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/delegates.go | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/version.go b/common/version.go index e7850e1a..b5d63be1 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.4` + Version = `v10.4.5` ) diff --git a/docs/changelog.md b/docs/changelog.md index 35d478d2..c6fb8ec8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.4.5 (date: 10.8.2021) + +- bugfix: removing one more filesystem sync from holotree (Mac slowdown fix). + ## v10.4.4 (date: 9.8.2021) - bugfix: raising initial scaling factor to 16, so that there should always diff --git a/htfs/delegates.go b/htfs/delegates.go index c1eaf48a..b31a0f46 100644 --- a/htfs/delegates.go +++ b/htfs/delegates.go @@ -24,7 +24,6 @@ func delegateOpen(it MutableLibrary, digest string) (readable io.Reader, closer } closer = func() error { reader.Close() - source.Sync() return source.Close() } return reader, closer, nil From 6aecb33f96bd69124e1be6f18227b8d1eb9dedb0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 10 Aug 2021 17:44:46 +0300 Subject: [PATCH 167/516] RCC-187: multiple environment configs (v10.5.0) - supporting multiple environment configurations to enable operating system and architecture specific freeze files (within one robot project) --- cmd/rcc/main.go | 2 +- common/version.go | 2 +- docs/changelog.md | 11 +++++++--- robot/robot.go | 54 +++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index e10fb644..5b44307d 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -60,7 +60,7 @@ func markTempForRecycling() { markedAlready = true filename := filepath.Join(conda.RobocorpTemp(), "recycle.now") ioutil.WriteFile(filename, []byte("True"), 0o644) - common.Debug("Marked %q for recyling.", conda.RobocorpTemp()) + common.Debug("Marked %q for recycling.", conda.RobocorpTemp()) } func main() { diff --git a/common/version.go b/common/version.go index b5d63be1..710d3b72 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.4.5` + Version = `v10.5.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index c6fb8ec8..c50a0795 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,20 +1,25 @@ # rcc change log +## v10.5.0 (date: 10.8.2021) + +- supporting multiple environment configurations to enable operating system + and architecture specific freeze files (within one robot project) + ## v10.4.5 (date: 10.8.2021) - bugfix: removing one more filesystem sync from holotree (Mac slowdown fix). -## v10.4.4 (date: 9.8.2021) +## v10.4.4 (date: 9.8.2021) broken - bugfix: raising initial scaling factor to 16, so that there should always be workers waiting -## v10.4.3 (date: 9.8.2021) +## v10.4.3 (date: 9.8.2021) broken - bugfix: trying to fix Mac related slowing by removing file syncs on holotree copy processes -## v10.4.2 (date: 5.8.2021) +## v10.4.2 (date: 5.8.2021) broken - bugfix: scaling down holotree concurrency, since at least Mac file limits are hit by current concurrency limit diff --git a/robot/robot.go b/robot/robot.go index c3c75bbc..c1dc0e8a 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -48,13 +48,14 @@ type Task interface { } type robot struct { - Tasks map[string]*task `yaml:"tasks"` - Conda string `yaml:"condaConfigFile"` - Ignored []string `yaml:"ignoreFiles"` - Artifacts string `yaml:"artifactsDir"` - Path []string `yaml:"PATH"` - Pythonpath []string `yaml:"PYTHONPATH"` - Root string + Tasks map[string]*task `yaml:"tasks"` + Conda string `yaml:"condaConfigFile,omitempty"` + Environments []string `yaml:"environmentConfigs,omitempty"` + Ignored []string `yaml:"ignoreFiles"` + Artifacts string `yaml:"artifactsDir"` + Path []string `yaml:"PATH"` + Pythonpath []string `yaml:"PYTHONPATH"` + Root string } type task struct { @@ -309,10 +310,14 @@ func (it *robot) TaskByName(name string) Task { } func (it *robot) UsesConda() bool { - return len(it.Conda) > 0 + return len(it.Conda) > 0 || len(it.availableEnvironmentConfigurations(osArchitectureToken())) > 0 } func (it *robot) CondaConfigFile() string { + available := it.availableEnvironmentConfigurations(osArchitectureToken()) + if len(available) > 0 { + return available[0] + } return filepath.Join(it.Root, it.Conda) } @@ -328,8 +333,39 @@ func (it *robot) WorkingDirectory() string { return it.Root } +func osArchitectureToken() string { + return fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) +} + +func freezeFileBasename() string { + return fmt.Sprintf("environment_%s_freeze.yaml", osArchitectureToken()) +} + +func (it *robot) availableEnvironmentConfigurations(marker string) []string { + result := make([]string, 0, len(it.Environments)) + common.Trace("Available environment configurations:") + for _, part := range it.Environments { + underscored := strings.Count(part, "_") > 2 + freezed := strings.Contains(strings.ToLower(part), "freeze") + marked := strings.Contains(part, marker) + if (underscored || freezed) && !marked { + continue + } + fullpath := filepath.Join(it.Root, part) + if !pathlib.IsFile(fullpath) { + continue + } + common.Trace("- %s", fullpath) + result = append(result, fullpath) + } + if len(result) == 0 { + common.Trace("- nothing") + } + return result +} + func (it *robot) FreezeFilename() string { - return filepath.Join(it.ArtifactDirectory(), fmt.Sprintf("environment_%s_%s_freeze.yaml", runtime.GOOS, runtime.GOARCH)) + return filepath.Join(it.ArtifactDirectory(), freezeFileBasename()) } func (it *robot) ArtifactDirectory() string { From 18ee1cdd5e0acf7f0be02eed3192a96756b25579 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 11 Aug 2021 12:08:55 +0300 Subject: [PATCH 168/516] RCC-187: multiple environment configurations (v10.5.1) - improvements for detecting OS/architecture for multiple environment configurations --- common/version.go | 2 +- docs/changelog.md | 5 +++++ robot/robot.go | 17 +++++++++++++++++ robot/robot_test.go | 9 +++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index 710d3b72..dcb667c3 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.5.0` + Version = `v10.5.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index c50a0795..9b74e851 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.5.1 (date: 11.8.2021) + +- improvements for detecting OS/architecture for multiple environment + configurations + ## v10.5.0 (date: 10.8.2021) - supporting multiple environment configurations to enable operating system diff --git a/robot/robot.go b/robot/robot.go index c1dc0e8a..e067448b 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "runtime" "sort" "strings" @@ -19,6 +20,11 @@ import ( "gopkg.in/yaml.v2" ) +var ( + GoosPattern = regexp.MustCompile("(?i:(windows|darwin|linux))") + GoarchPattern = regexp.MustCompile("(?i:(amd64|arm64))") +) + type Robot interface { IgnoreFiles() []string AvailableTasks() []string @@ -341,6 +347,11 @@ func freezeFileBasename() string { return fmt.Sprintf("environment_%s_freeze.yaml", osArchitectureToken()) } +func submatch(pattern *regexp.Regexp, expected, text string) bool { + match := pattern.FindStringSubmatch(text) + return match == nil || len(match) == 0 || match[0] == expected +} + func (it *robot) availableEnvironmentConfigurations(marker string) []string { result := make([]string, 0, len(it.Environments)) common.Trace("Available environment configurations:") @@ -351,6 +362,12 @@ func (it *robot) availableEnvironmentConfigurations(marker string) []string { if (underscored || freezed) && !marked { continue } + if !submatch(GoosPattern, runtime.GOOS, part) { + continue + } + if !submatch(GoarchPattern, runtime.GOARCH, part) { + continue + } fullpath := filepath.Join(it.Root, part) if !pathlib.IsFile(fullpath) { continue diff --git a/robot/robot_test.go b/robot/robot_test.go index 3f013561..b413dd4d 100644 --- a/robot/robot_test.go +++ b/robot/robot_test.go @@ -16,6 +16,15 @@ func TestCannotReadMissingRobotYaml(t *testing.T) { must.Nil(sut) } +func TestCanMatchArchitecture(t *testing.T) { + must, wont := hamlet.Specifications(t) + + wont.Nil(robot.GoosPattern) + wont.Nil(robot.GoarchPattern) + must.Equal([]string{"darwin", "darwin"}, robot.GoosPattern.FindStringSubmatch("foo_darwin_arm64_freeze.yaml")) + must.Equal([]string{"arm64", "arm64"}, robot.GoarchPattern.FindStringSubmatch("foo_darwin_arm64_freeze.yaml")) +} + func TestCanReadRealRobotYaml(t *testing.T) { must, wont := hamlet.Specifications(t) From 5a0151fb11efcb62bfa3c72a2639026d1762ac16 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 12 Aug 2021 09:34:11 +0300 Subject: [PATCH 169/516] RCC-148: timezone metric (v10.5.2) - added once a day metric about timezone where rcc is executed --- cmd/rcc/main.go | 22 ++++++++++++++++++++++ common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/cache.go | 5 +++++ robot_tests/fullrun.robot | 2 +- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 5b44307d..cf37bac8 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -10,13 +10,34 @@ import ( "github.com/robocorp/rcc/cmd" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" ) +const ( + timezonekey = `rcc.cli.tz` + daily = 60 * 60 * 24 +) + var ( markedAlready = false ) +func TimezoneMetric() error { + cache, err := operations.SummonCache() + if err != nil { + return err + } + deadline, ok := cache.Stamps[timezonekey] + if ok && deadline > common.When { + return nil + } + cache.Stamps[timezonekey] = common.When + daily + zone := time.Now().Format("MST-0700") + cloud.BackgroundMetric(common.ControllerIdentity(), timezonekey, zone) + return cache.Save() +} + func ExitProtection() { status := recover() if status != nil { @@ -73,4 +94,5 @@ func main() { defer os.Stderr.Sync() defer os.Stdout.Sync() cmd.Execute() + TimezoneMetric() } diff --git a/common/version.go b/common/version.go index dcb667c3..c9fa057a 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.5.1` + Version = `v10.5.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 9b74e851..75b85d14 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.5.2 (date: 12.8.2021) + +- added once a day metric about timezone where rcc is executed + ## v10.5.1 (date: 11.8.2021) - improvements for detecting OS/architecture for multiple environment diff --git a/operations/cache.go b/operations/cache.go index ae4a4273..706fba87 100644 --- a/operations/cache.go +++ b/operations/cache.go @@ -28,10 +28,12 @@ type Credential struct { type FolderMap map[string]*Folder type CredentialMap map[string]*Credential +type StampMap map[string]int64 type Cache struct { Robots FolderMap `yaml:"robots"` Credentials CredentialMap `yaml:"credentials"` + Stamps StampMap `yaml:"stamps"` } func (it Cache) Ready() *Cache { @@ -41,6 +43,9 @@ func (it Cache) Ready() *Cache { if it.Credentials == nil { it.Credentials = make(CredentialMap) } + if it.Stamps == nil { + it.Stamps = make(StampMap) + } return &it } diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index cd31744c..81ab79b1 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -21,7 +21,7 @@ Goal: Telemetry tracking enabled by default. Step build/rcc configure identity --controller citests Must Have anonymous health tracking is: enabled Must Exist %{ROBOCORP_HOME}/rcc.yaml - Wont Exist %{ROBOCORP_HOME}/rcccache.yaml + Must Exist %{ROBOCORP_HOME}/rcccache.yaml Goal: Send telemetry data to cloud. Step build/rcc feedback metric --controller citests -t test -n rcc.test -v robot.fullrun From e617f014afbad8ee2280c804a31a387ae9c49286 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 16 Aug 2021 12:13:33 +0300 Subject: [PATCH 170/516] RCC-192: holotree space deletion improvement (v10.6.0) - added possibility to also delete holotree space by providing controller and space flags (for easier scripting) --- cmd/holotreeDelete.go | 34 +++++++++++++++++++++++----------- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/library.go | 10 +++++++--- htfs/virtual.go | 4 +--- htfs/ziplibrary.go | 4 +--- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go index 44545c19..7902d38d 100644 --- a/cmd/holotreeDelete.go +++ b/cmd/holotreeDelete.go @@ -8,26 +8,38 @@ import ( "github.com/spf13/cobra" ) +func deleteByPartialIdentity(partials []string) { + for _, prefix := range partials { + for _, label := range htfs.FindEnvironment(prefix) { + common.Log("Removing %v", label) + if dryFlag { + continue + } + err := htfs.RemoveHolotreeSpace(label) + pretty.Guard(err == nil, 1, "Error: %v", err) + } + } +} + var holotreeDeleteCmd = &cobra.Command{ - Use: "delete +", + Use: "delete *", Short: "Delete holotree controller space.", Long: "Delete holotree controller space.", - Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - for _, prefix := range args { - for _, label := range htfs.FindEnvironment(prefix) { - common.Log("Removing %v", label) - if dryFlag { - continue - } - err := htfs.RemoveHolotreeSpace(label) - pretty.Guard(err == nil, 1, "Error: %v", err) - } + partials := make([]string, 0, len(args)+1) + if len(args) > 0 { + partials = append(partials, args...) + } + if len(common.HolotreeSpace) > 0 { + partials = append(partials, htfs.ControllerSpaceName([]byte(common.ControllerIdentity()), []byte(common.HolotreeSpace))) } + pretty.Guard(len(partials) > 0, 1, "Must provide either --space flag, or partial environment identity!") + deleteByPartialIdentity(partials) }, } func init() { holotreeCmd.AddCommand(holotreeDeleteCmd) holotreeDeleteCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") + holotreeDeleteCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify environment to delete.") } diff --git a/common/version.go b/common/version.go index c9fa057a..d7df83bb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.5.2` + Version = `v10.6.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index 75b85d14..32e616ae 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.6.0 (date: 16.8.2021) + +- added possibility to also delete holotree space by providing controller + and space flags (for easier scripting) + ## v10.5.2 (date: 12.8.2021) - added once a day metric about timezone where rcc is executed diff --git a/htfs/library.go b/htfs/library.go index 8f952795..69677616 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -261,15 +261,19 @@ func Spaces() []*Root { return roots } +func ControllerSpaceName(client, tag []byte) string { + prefix := textual(sipit(client), 9) + suffix := textual(sipit(tag), 8) + return prefix + "_" + suffix +} + func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err error) { defer fail.Around(&err) defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) catalog := it.CatalogPath(key) common.Timeline("holotree restore start %s", key) - prefix := textual(sipit(client), 9) - suffix := textual(sipit(tag), 8) - name := prefix + "_" + suffix + name := ControllerSpaceName(client, tag) fs, err := NewRoot(it.Stage()) fail.On(err != nil, "Failed to create stage -> %v", err) err = fs.LoadFrom(catalog) diff --git a/htfs/virtual.go b/htfs/virtual.go index 99f786ca..17b91cf1 100644 --- a/htfs/virtual.go +++ b/htfs/virtual.go @@ -67,9 +67,7 @@ func (it *virtual) Restore(blueprint, client, tag []byte) (string, error) { defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) common.Timeline("holotree restore start %s (virtual)", key) - prefix := textual(sipit(client), 9) - suffix := textual(sipit(tag), 8) - name := prefix + "_" + suffix + name := ControllerSpaceName(client, tag) metafile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.meta", name)) targetdir := filepath.Join(common.HolotreeLocation(), name) lockfile := filepath.Join(common.HolotreeLocation(), fmt.Sprintf("%s.lck", name)) diff --git a/htfs/ziplibrary.go b/htfs/ziplibrary.go index 66d2bcae..6d443133 100644 --- a/htfs/ziplibrary.go +++ b/htfs/ziplibrary.go @@ -79,9 +79,7 @@ func (it *ziplibrary) Restore(blueprint, client, tag []byte) (result string, err defer common.Stopwatch("Holotree restore took:").Debug() key := BlueprintHash(blueprint) common.Timeline("holotree restore start %s (zip)", key) - prefix := textual(sipit(client), 9) - suffix := textual(sipit(tag), 8) - name := prefix + "_" + suffix + name := ControllerSpaceName(client, tag) fs, err := NewRoot(".") fail.On(err != nil, "Failed to create root -> %v", err) catalog := it.CatalogPath(key) From b336a68f49e5274714ded0b31c3e3df3bf65eabf Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 17 Aug 2021 10:27:15 +0300 Subject: [PATCH 171/516] RCC-190: visible command serialization (v10.7.0) - when environment creation is serialized, after short delay, rcc reports that it is waiting to be able to contiue - added __MACOSX as ignored files/directories --- common/version.go | 2 +- conda/workflows.go | 2 ++ docs/changelog.md | 6 ++++++ htfs/commands.go | 2 ++ htfs/directory.go | 1 + operations/zipper.go | 1 + pathlib/lock.go | 25 +++++++++++++++++++++++++ 7 files changed, 38 insertions(+), 1 deletion(-) diff --git a/common/version.go b/common/version.go index d7df83bb..9e8c23ac 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.6.0` + Version = `v10.7.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index 284a04e3..e51e41c6 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -352,7 +352,9 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() + callback := pathlib.LockWaitMessage("Serialized environment creation") locker, err := pathlib.Locker(lockfile, 30000) + callback() if err != nil { common.Log("Could not get lock on live environment. Quitting!") return "", err diff --git a/docs/changelog.md b/docs/changelog.md index 32e616ae..eaaba320 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v10.7.0 (date: 16.8.2021) + +- when environment creation is serialized, after short delay, rcc reports + that it is waiting to be able to contiue +- added __MACOSX as ignored files/directories + ## v10.6.0 (date: 16.8.2021) - added possibility to also delete holotree space by providing controller diff --git a/htfs/commands.go b/htfs/commands.go index abbd6a3b..4fcbe18e 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -23,7 +23,9 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) + callback := pathlib.LockWaitMessage("Serialized environment creation") locker, err := pathlib.Locker(common.HolotreeLock(), 30000) + callback() fail.On(err != nil, "Could not get lock for holotree. Quiting.") defer locker.Release() diff --git a/htfs/directory.go b/htfs/directory.go index f31002a1..a908bf00 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -21,6 +21,7 @@ var ( func init() { killfile = make(map[string]bool) + killfile["__MACOSX"] = true killfile["__pycache__"] = true killfile[".pyc"] = true killfile[".git"] = true diff --git a/operations/zipper.go b/operations/zipper.go index a1cb305b..d03c5e79 100644 --- a/operations/zipper.go +++ b/operations/zipper.go @@ -228,6 +228,7 @@ func defaultIgnores(selfie string) pathlib.Ignore { result = append(result, pathlib.IgnorePattern("temp/")) result = append(result, pathlib.IgnorePattern("tmp/")) result = append(result, pathlib.IgnorePattern("__pycache__")) + result = append(result, pathlib.IgnorePattern("__MACOSX")) return pathlib.CompositeIgnore(result...) } diff --git a/pathlib/lock.go b/pathlib/lock.go index 807e8518..bc105817 100644 --- a/pathlib/lock.go +++ b/pathlib/lock.go @@ -2,6 +2,7 @@ package pathlib import ( "os" + "time" "github.com/robocorp/rcc/common" ) @@ -24,3 +25,27 @@ func Fake() Releaser { common.Trace("LOCKER: lockless mode.") return fake(true) } + +func waitingLockNotification(message string, latch chan bool) { + delay := 5 * time.Second + counter := 0 + for { + select { + case <-latch: + return + case <-time.After(delay): + counter += 1 + delay *= 3 + common.Log("#%d: %s (lock wait)", counter, message) + common.Timeline("waiting for lock") + } + } +} + +func LockWaitMessage(message string) func() { + latch := make(chan bool) + go waitingLockNotification(message, latch) + return func() { + latch <- true + } +} From ce4317366fe52c726f5ad407b90235fe68d90876 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 18 Aug 2021 16:26:34 +0300 Subject: [PATCH 172/516] BUGFIX: windows performance hit (v10.7.1) - bugfix: trying to remove preformance hit on windows directory cleanup --- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/commands.go | 23 ++++++++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 9e8c23ac..1478a6b6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.7.0` + Version = `v10.7.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index eaaba320..f1008669 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.7.1 (date: 18.8.2021) + +- bugfix: trying to remove preformance hit on windows directory cleanup + ## v10.7.0 (date: 16.8.2021) - when environment creation is serialized, after short delay, rcc reports diff --git a/htfs/commands.go b/htfs/commands.go index 4fcbe18e..40aff09c 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -77,6 +77,12 @@ func RecordCondaEnvironment(tree MutableLibrary, condafile string, force bool) ( return RecordEnvironment(tree, []byte(content), force) } +func CleanupHolotreeStage(tree MutableLibrary) error { + common.Timeline("holotree stage removal start") + defer common.Timeline("holotree stage removal done") + return os.RemoveAll(tree.Stage()) +} + func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err error) { defer fail.Around(&err) @@ -85,29 +91,28 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e common.Stageonly = true common.Liveonly = true - err = os.RemoveAll(tree.Stage()) - fail.On(err != nil, "Failed to clean stage, reason %v.", err) - - err = os.MkdirAll(tree.Stage(), 0o755) - fail.On(err != nil, "Failed to create stage, reason %v.", err) - common.Debug("Holotree stage is %q.", tree.Stage()) exists := tree.HasBlueprint(blueprint) common.Debug("Has blueprint environment: %v", exists) if force || !exists { + err = CleanupHolotreeStage(tree) + fail.On(err != nil, "Failed to clean stage, reason %v.", err) + + err = os.MkdirAll(tree.Stage(), 0o755) + fail.On(err != nil, "Failed to create stage, reason %v.", err) + identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = ioutil.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) label, err := conda.NewEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) common.Debug("Label: %q", label) - } - if force || !exists { - err := tree.Record(blueprint) + err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } + return nil } From b77a0a3a48b29dacf57e43b2f67fd5939ac61d63 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 19 Aug 2021 13:43:29 +0300 Subject: [PATCH 173/516] RCC-169: holotree library check (v10.8.0) - added holotree check command to verify holotree library integrity - added "env cleanup" also as "config cleanup" - minor go-routine schedule yield added (experiment) --- cloud/metrics.go | 2 ++ cmd/cleanup.go | 1 + cmd/holotreeCheck.go | 36 ++++++++++++++++++++++++++++++++++ cmd/rcc/main.go | 2 ++ common/version.go | 2 +- conda/workflows.go | 2 ++ docs/changelog.md | 6 ++++++ htfs/directory.go | 9 ++++++--- htfs/functions.go | 42 ++++++++++++++++++++++++++++++++++++++++ operations/robotcache.go | 2 ++ 10 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 cmd/holotreeCheck.go diff --git a/cloud/metrics.go b/cloud/metrics.go index b1bd0b80..a304a213 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -3,6 +3,7 @@ package cloud import ( "fmt" "net/url" + "runtime" "sync" "time" @@ -48,6 +49,7 @@ func BackgroundMetric(kind, name, value string) { if xviper.CanTrack() { telemetryBarrier.Add(1) go sendMetric(metricsHost, kind, name, value) + runtime.Gosched() } } diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 8ac57619..1f376e07 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -34,6 +34,7 @@ After cleanup, they will not be available anymore.`, } func init() { + configureCmd.AddCommand(cleanupCmd) envCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go new file mode 100644 index 00000000..e7364ce7 --- /dev/null +++ b/cmd/holotreeCheck.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "fmt" + + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + "github.com/spf13/cobra" +) + +var holotreeCheckCmd = &cobra.Command{ + Use: "check", + Short: "Check holotree library integrity.", + Long: "Check holotree library integrity.", + Run: func(cmd *cobra.Command, args []string) { + fs, err := htfs.NewRoot(common.HololibLibraryLocation()) + pretty.Guard(err == nil, 1, "%s", err) + err = fs.Lift() + pretty.Guard(err == nil, 2, "%s", err) + err = fs.AllFiles(htfs.Hasher()) + pretty.Guard(err == nil, 3, "%s", err) + collector := make(map[string]string) + err = fs.Treetop(htfs.IntegrityCheck(collector)) + pretty.Guard(err == nil, 4, "%s", err) + for k, v := range collector { + fmt.Println(k, v) + } + pretty.Guard(len(collector) == 0, 5, "Size: %d", len(collector)) + pretty.Ok() + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeCheckCmd) +} diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index cf37bac8..45fbb6d7 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "time" "github.com/robocorp/rcc/cloud" @@ -72,6 +73,7 @@ func startTempRecycling() { go os.RemoveAll(folder) } } + runtime.Gosched() } func markTempForRecycling() { diff --git a/common/version.go b/common/version.go index 1478a6b6..b8e1d838 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.7.1` + Version = `v10.8.0` ) diff --git a/conda/workflows.go b/conda/workflows.go index e51e41c6..9b7fe5a0 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -9,6 +9,7 @@ import ( "math/rand" "os" "path/filepath" + "runtime" "strings" "time" @@ -581,6 +582,7 @@ func cloneFolder(source, target string, workers int, copier pathlib.Copier) bool for x := 0; x < workers; x++ { go copyWorker(queue, done, copier) } + runtime.Gosched() success := copyFolder(source, target, queue) close(queue) diff --git a/docs/changelog.md b/docs/changelog.md index f1008669..724d5cfa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v10.8.0 (date: 19.8.2021) + +- added holotree check command to verify holotree library integrity +- added "env cleanup" also as "config cleanup" +- minor go-routine schedule yield added (experiment) + ## v10.7.1 (date: 18.8.2021) - bugfix: trying to remove preformance hit on windows directory cleanup diff --git a/htfs/directory.go b/htfs/directory.go index a908bf00..67d2547d 100644 --- a/htfs/directory.go +++ b/htfs/directory.go @@ -98,23 +98,26 @@ func (it *Root) Lift() error { } func (it *Root) Treetop(task Treetop) error { + common.Timeline("holotree treetop sync start") + defer common.Timeline("holotree treetop sync done") err := task(it.Path, it.Tree) if err != nil { return err } - common.Timeline("holotree treetop sync") return anywork.Sync() } func (it *Root) AllDirs(task Dirtask) error { + common.Timeline("holotree dirs sync start") + defer common.Timeline("holotree dirs sync done") it.Tree.AllDirs(it.Path, task) - common.Timeline("holotree dirs sync") return anywork.Sync() } func (it *Root) AllFiles(task Filetask) error { + common.Timeline("holotree files sync start") + defer common.Timeline("holotree files sync done") it.Tree.AllFiles(it.Path, task) - common.Timeline("holotree files sync") return anywork.Sync() } diff --git a/htfs/functions.go b/htfs/functions.go index 74260a18..8ff6b720 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -64,6 +64,48 @@ func DigestRecorder(target map[string]string) Treetop { return tool } +func IntegrityCheck(result map[string]string) Treetop { + var tool Treetop + tool = func(path string, it *Dir) error { + for name, subdir := range it.Dirs { + tool(filepath.Join(path, name), subdir) + } + for name, file := range it.Files { + if file.Name != file.Digest { + result[filepath.Join(path, name)] = file.Digest + } + } + return nil + } + return tool +} + +func Hasher() Filetask { + return func(fullpath string, details *File) anywork.Work { + return func() { + source, err := os.Open(fullpath) + if err != nil { + panic(fmt.Sprintf("Open %q, reason: %v", fullpath, err)) + } + defer source.Close() + + var reader io.ReadCloser + reader, err = gzip.NewReader(source) + if err != nil { + _, err = source.Seek(0, 0) + fail.On(err != nil, "Failed to seek %q -> %v", fullpath, err) + reader = source + } + digest := sha256.New() + _, err = io.Copy(digest, reader) + if err != nil { + panic(fmt.Sprintf("Copy %q, reason: %v", fullpath, err)) + } + details.Digest = fmt.Sprintf("%02x", digest.Sum(nil)) + } + } +} + func Locator(seek string) Filetask { return func(fullpath string, details *File) anywork.Work { return func() { diff --git a/operations/robotcache.go b/operations/robotcache.go index f16874a3..58b8cabf 100644 --- a/operations/robotcache.go +++ b/operations/robotcache.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "time" "github.com/robocorp/rcc/common" @@ -45,6 +46,7 @@ func CacheRobot(filename string) error { return fmt.Errorf("Could not cache %v, reason: digest mismatch.", fullpath) } go CleanupOldestRobot() + runtime.Gosched() return nil } From aaee8406cd000c4244002c5638de086097a6e537 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 24 Aug 2021 15:27:00 +0300 Subject: [PATCH 174/516] RCC-169: holotree library check (v10.8.1) - holotree check command now removes orphan hololib files - environment creation metrics added on failure cases - pip and micromamba exit codes now also in hex form - minor error message fixes for Windows (colors) --- cmd/holotreeCheck.go | 37 ++++++++++++++-------- common/version.go | 2 +- conda/workflows.go | 20 ++++++------ docs/changelog.md | 7 +++++ htfs/functions.go | 68 ++++++++++++++++++++++++++++++++++++++++- htfs/library.go | 1 + pretty/setup_windows.go | 8 ++--- 7 files changed, 115 insertions(+), 28 deletions(-) diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index e7364ce7..d92eef11 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -9,24 +9,35 @@ import ( "github.com/spf13/cobra" ) +func checkHolotreeIntegrity() { + common.Timeline("holotree integrity check start") + defer common.Timeline("holotree integrity check done") + fs, err := htfs.NewRoot(common.HololibLibraryLocation()) + pretty.Guard(err == nil, 1, "%s", err) + common.Timeline("holotree integrity lift") + err = fs.Lift() + pretty.Guard(err == nil, 2, "%s", err) + common.Timeline("holotree integrity hasher") + known := htfs.LoadHololibHashes() + err = fs.AllFiles(htfs.Hasher(known)) + pretty.Guard(err == nil, 3, "%s", err) + collector := make(map[string]string) + common.Timeline("holotree integrity collector") + err = fs.Treetop(htfs.IntegrityCheck(collector)) + common.Timeline("holotree integrity report") + pretty.Guard(err == nil, 4, "%s", err) + for k, v := range collector { + fmt.Println(k, v) + } + pretty.Guard(len(collector) == 0, 5, "Size: %d", len(collector)) +} + var holotreeCheckCmd = &cobra.Command{ Use: "check", Short: "Check holotree library integrity.", Long: "Check holotree library integrity.", Run: func(cmd *cobra.Command, args []string) { - fs, err := htfs.NewRoot(common.HololibLibraryLocation()) - pretty.Guard(err == nil, 1, "%s", err) - err = fs.Lift() - pretty.Guard(err == nil, 2, "%s", err) - err = fs.AllFiles(htfs.Hasher()) - pretty.Guard(err == nil, 3, "%s", err) - collector := make(map[string]string) - err = fs.Treetop(htfs.IntegrityCheck(collector)) - pretty.Guard(err == nil, 4, "%s", err) - for k, v := range collector { - fmt.Println(k, v) - } - pretty.Guard(len(collector) == 0, 5, "Size: %d", len(collector)) + checkHolotreeIntegrity() pretty.Ok() }, } diff --git a/common/version.go b/common/version.go index b8e1d838..e2431450 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.8.0` + Version = `v10.8.1` ) diff --git a/conda/workflows.go b/conda/workflows.go index 9b7fe5a0..1158f80c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -108,15 +108,14 @@ func LiveCapture(liveFolder string, command ...string) (string, int, error) { return task.CaptureOutput() } -func LiveExecution(sink *os.File, liveFolder string, command ...string) error { +func LiveExecution(sink *os.File, liveFolder string, command ...string) (int, error) { defer sink.Sync() fmt.Fprintf(sink, "Command %q at %q:\n", command, liveFolder) task, err := livePrepare(liveFolder, command...) if err != nil { - return err + return 0, err } - _, err = task.Tracked(sink, false) - return err + return task.Tracked(sink, false) } type InstallObserver map[string]bool @@ -162,6 +161,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall common.Timeline("first try.") success, fatal := newLiveInternal(yaml, condaYaml, requirementsText, key, force, freshInstall, postInstall) if !success && !force && !fatal { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.retry", common.Version) common.Debug("=== new live --- second try phase ===") common.Timeline("second try.") common.ForceDebug() @@ -210,8 +210,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh tee := io.MultiWriter(observer, planWriter) code, err := shell.New(CondaEnvironment(), ".", mambaCommand.CLI()...).Tracked(tee, false) if err != nil || code != 0 { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.micromamba", fmt.Sprintf("%d_%x", code, code)) common.Timeline("micromamba fail.") - common.Fatal("Micromamba", err) + common.Fatal(fmt.Sprintf("Micromamba [%d/%x]", code, code), err) return false, false } common.Timeline("micromamba done.") @@ -233,10 +234,11 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipCommand.Option("--trusted-host", settings.Global.PypiTrustedHost()) pipCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") common.Debug("=== new live --- pip install phase ===") - err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) - if err != nil { + code, err = LiveExecution(planWriter, targetFolder, pipCommand.CLI()...) + if err != nil || code != 0 { + cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.fatal.pip", fmt.Sprintf("%d_%x", code, code)) common.Timeline("pip fail.") - common.Fatal("Pip", err) + common.Fatal(fmt.Sprintf("Pip [%d/%x]", code, code), err) return false, false } common.Timeline("pip done.") @@ -254,7 +256,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } common.Debug("Running post install script '%s' ...", script) - err = LiveExecution(planWriter, targetFolder, scriptCommand...) + _, err = LiveExecution(planWriter, targetFolder, scriptCommand...) if err != nil { common.Fatal("post-install", err) common.Log("%sScript '%s' failure: %v%s", pretty.Red, script, err, pretty.Reset) diff --git a/docs/changelog.md b/docs/changelog.md index 724d5cfa..cd7068d0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v10.8.1 (date: 20.8.2021) + +- holotree check command now removes orphan hololib files +- environment creation metrics added on failure cases +- pip and micromamba exit codes now also in hex form +- minor error message fixes for Windows (colors) + ## v10.8.0 (date: 19.8.2021) - added holotree check command to verify holotree library integrity diff --git a/htfs/functions.go b/htfs/functions.go index 8ff6b720..cc6b9472 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -7,9 +7,11 @@ import ( "io" "os" "path/filepath" + "runtime" "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/trollhash" @@ -80,9 +82,13 @@ func IntegrityCheck(result map[string]string) Treetop { return tool } -func Hasher() Filetask { +func Hasher(known map[string]string) Filetask { return func(fullpath string, details *File) anywork.Work { return func() { + _, ok := known[details.Name] + if !ok { + defer anywork.Backlog(RemoveFile(fullpath)) + } source, err := os.Open(fullpath) if err != nil { panic(fmt.Sprintf("Open %q, reason: %v", fullpath, err)) @@ -371,3 +377,63 @@ func ZipRoot(library MutableLibrary, fs *Root, sink Zipper) Treetop { } return tool } + +func LoadHololibHashes() map[string]string { + roots := LoadCatalogs() + slots := make([]map[string]string, len(roots)) + for at, root := range roots { + anywork.Backlog(DigestLoader(root, at, slots)) + } + result := make(map[string]string) + runtime.Gosched() + anywork.Sync() + for _, slot := range slots { + for k, v := range slot { + result[k] = v + } + } + return result +} + +func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { + return func() { + collector := make(map[string]string) + task := DigestMapper(collector) + err := task(root.Path, root.Tree) + if err != nil { + panic(fmt.Sprintf("Collecting dir %q, reason: %v", root.Path, err)) + } + slots[at] = collector + common.Trace("Root %q loaded.", root.Path) + } +} + +func LoadCatalogs() []*Root { + common.Timeline("catalog load start") + defer common.Timeline("catalog load done") + catalogs := Catalogs() + roots := make([]*Root, len(catalogs)) + for at, catalog := range catalogs { + fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) + anywork.Backlog(CatalogLoader(fullpath, at, roots)) + } + runtime.Gosched() + anywork.Sync() + return roots +} + +func CatalogLoader(catalog string, at int, roots []*Root) anywork.Work { + return func() { + tempdir := filepath.Join(conda.RobocorpTemp(), "shadow") + shadow, err := NewRoot(tempdir) + if err != nil { + panic(fmt.Sprintf("Temp dir %q, reason: %v", tempdir, err)) + } + err = shadow.LoadFrom(catalog) + if err != nil { + panic(fmt.Sprintf("Load %q, reason: %v", catalog, err)) + } + roots[at] = shadow + common.Trace("Catalog %q loaded.", catalog) + } +} diff --git a/htfs/library.go b/htfs/library.go index 69677616..2d383186 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -312,6 +312,7 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er fs.Space = string(tag) err = fs.SaveAs(metafile) fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) + pathlib.TouchWhen(catalog, time.Now()) return targetdir, nil } diff --git a/pretty/setup_windows.go b/pretty/setup_windows.go index 31cfafdf..449dc3f2 100644 --- a/pretty/setup_windows.go +++ b/pretty/setup_windows.go @@ -20,24 +20,24 @@ func localSetup(interactive bool) { } kernel32 := syscall.NewLazyDLL("kernel32.dll") if kernel32 == nil { - common.Trace("Cannot use colors. Did not get kernel32.dll!") + common.Trace("Error: Cannot use colors. Did not get kernel32.dll!") return } setConsoleMode := kernel32.NewProc("SetConsoleMode") if setConsoleMode == nil { - common.Trace("Cannot use colors. Did not get SetConsoleMode!") + common.Trace("Error: Cannot use colors. Did not get SetConsoleMode!") return } target := syscall.Stdout var mode uint32 err := syscall.GetConsoleMode(target, &mode) if err != nil { - common.Trace("Cannot use colors. Got mode error '%v'!", err) + common.Trace("Error: Cannot use colors. Got mode error '%v'!", err) } mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING success, _, err := setConsoleMode.Call(uintptr(target), uintptr(mode)) Disabled = success == 0 if Disabled && err != nil { - common.Trace("Cannot use colors. Got error '%v'!", err) + common.Trace("Error: Cannot use colors. Got error '%v'!", err) } } From 0f7df8142668cedaab7d678a1899a78c758e1c29 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 25 Aug 2021 15:24:25 +0300 Subject: [PATCH 175/516] RCC-193: quick environment cleanup (v10.9.0) - added --quick option to `rcc config cleanup` command to provide partial cleanup, but leave hololib and pkgs cache intact --- cmd/cleanup.go | 4 +++- common/version.go | 2 +- conda/cleanup.go | 28 +++++++++++++++++++++------- docs/changelog.md | 7 ++++++- robot_tests/fullrun.robot | 2 +- robot_tests/holotree.robot | 12 ++++++++++++ 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 1f376e07..e56fd4bc 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -10,6 +10,7 @@ import ( var ( allFlag bool + quickFlag bool orphanFlag bool minicondaFlag bool micromambaFlag bool @@ -25,7 +26,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, orphanFlag, allFlag, minicondaFlag, micromambaFlag) + err := conda.Cleanup(daysOption, dryFlag, orphanFlag, quickFlag, allFlag, minicondaFlag, micromambaFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -41,5 +42,6 @@ func init() { cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Cleanup all enviroments.") + cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") } diff --git a/common/version.go b/common/version.go index e2431450..90c5999d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.8.1` + Version = `v10.9.0` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 5eed9ac4..954826e7 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -57,27 +57,37 @@ func orphanCleanup(dryrun bool) error { return nil } -func spotlessCleanup(dryrun bool) error { +func quickCleanup(dryrun bool) error { if dryrun { common.Log("Would be removing:") common.Log("- %v", common.BaseLocation()) common.Log("- %v", common.LiveLocation()) common.Log("- %v", common.HolotreeLocation()) - common.Log("- %v", common.HololibLocation()) common.Log("- %v", common.PipCache()) - common.Log("- %v", MambaPackages()) - common.Log("- %v", BinMicromamba()) common.Log("- %v", RobocorpTempRoot()) + common.Log("- %v", MinicondaLocation()) return nil } safeRemove("cache", common.HolotreeLocation()) - safeRemove("cache", common.HololibLocation()) safeRemove("cache", common.BaseLocation()) safeRemove("cache", common.LiveLocation()) safeRemove("cache", common.PipCache()) - safeRemove("cache", MambaPackages()) safeRemove("temp", RobocorpTempRoot()) + safeRemove("cache", MinicondaLocation()) + return nil +} + +func spotlessCleanup(dryrun bool) error { + quickCleanup(dryrun) + if dryrun { + common.Log("- %v", BinMicromamba()) + common.Log("- %v", common.HololibLocation()) + common.Log("- %v", MambaPackages()) + return nil + } safeRemove("executable", BinMicromamba()) + safeRemove("cache", common.HololibLocation()) + safeRemove("cache", MambaPackages()) return nil } @@ -114,7 +124,7 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { return nil } -func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) error { +func Cleanup(daylimit int, dryrun, orphans, quick, all, miniconda, micromamba bool) error { lockfile := common.RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -123,6 +133,10 @@ func Cleanup(daylimit int, dryrun, orphans, all, miniconda, micromamba bool) err } defer locker.Release() + if quick { + return quickCleanup(dryrun) + } + if all { return spotlessCleanup(dryrun) } diff --git a/docs/changelog.md b/docs/changelog.md index cd7068d0..04064dfe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # rcc change log -## v10.8.1 (date: 20.8.2021) +## v10.9.0 (date: 25.8.2021) + +- added --quick option to `rcc config cleanup` command to provide + partial cleanup, but leave hololib and pkgs cache intact + +## v10.8.1 (date: 24.8.2021) - holotree check command now removes orphan hololib files - environment creation metrics added on failure cases diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 81ab79b1..cec6e217 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -217,7 +217,7 @@ Goal: See diagnostics as valid JSON form Must Be Json Response Goal: Simulate issue report sending with dryrun - Step build/rcc feedback issue --dryrun --report robot_tests/report.json --attachments robot_tests/conda.yaml + Step build/rcc feedback issue --controller citests --dryrun --report robot_tests/report.json --attachments robot_tests/conda.yaml Must Have "report": Must Have "zipfile": Must Have "installationId": diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index adb8f9b2..ed63857d 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -121,6 +121,18 @@ Goal: Liveonly works and uses virtual holotree Must Have (virtual) Must Have live only +Goal: Do quick cleanup on environments + Step build/rcc config cleanup --controller citests --quick + Must Exist %{ROBOCORP_HOME}/bin/micromamba + Must Exist %{ROBOCORP_HOME}/hololib/ + Must Exist %{ROBOCORP_HOME}/pkgs/ + Wont Exist %{ROBOCORP_HOME}/holotree/ + Wont Exist %{ROBOCORP_HOME}/base/ + Wont Exist %{ROBOCORP_HOME}/live/ + Wont Exist %{ROBOCORP_HOME}/pipcache/ + Use STDERR + Must Have OK + Goal: Liveonly works and uses virtual holotree and can give output in JSON form Step build/rcc ht vars --liveonly --space jam --controller citests --json robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline Must Be Json Response From dc6dbb1dcbdb500646e9c5665d2af73ceb6368ac Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 27 Aug 2021 14:11:49 +0300 Subject: [PATCH 176/516] FIXES: assistant run improvements (v10.9.1) - made problems in assistant heartbeats visible - changed assistant heartbeat from 60s to 37s to prevent collision with DNS TTL value --- common/version.go | 2 +- docs/changelog.md | 6 ++++++ operations/assistant.go | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/common/version.go b/common/version.go index 90c5999d..8e8c0aed 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.9.0` + Version = `v10.9.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 04064dfe..7014cc26 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v10.9.1 (date: 27.8.2021) + +- made problems in assistant heartbeats visible +- changed assistant heartbeat from 60s to 37s to prevent collision with + DNS TTL value + ## v10.9.0 (date: 25.8.2021) - added --quick option to `rcc config cleanup` command to provide diff --git a/operations/assistant.go b/operations/assistant.go index 387782db..cc956b9d 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -248,14 +248,21 @@ func BackgroundAssistantHeartbeat(cancel chan bool, client cloud.Client, account case _ = <-cancel: common.Trace("Stopping assistant heartbeat.") return - case <-time.After(60 * time.Second): + case <-time.After(37 * time.Second): counter += 1 common.Trace("Sending assistant heartbeat #%d.", counter) - go BeatAssistantRun(client, account, workspaceId, assistantId, runId, counter) + go beatAssistantRunBackground(client, account, workspaceId, assistantId, runId, counter) } } } +func beatAssistantRunBackground(client cloud.Client, account *account, workspaceId, assistantId, runId string, beat int) { + err := BeatAssistantRun(client, account, workspaceId, assistantId, runId, beat) + if err != nil { + common.Log("Problem sendig assistant heartbeat: %s", err) + } +} + func BeatAssistantRun(client cloud.Client, account *account, workspaceId, assistantId, runId string, beat int) error { common.Timeline("send assistant heartbeat") credentials, err := summonAssistantToken(client, account, workspaceId) From 44f532a79fd172dd3f2442e0cf741c1061407aca Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 30 Aug 2021 12:15:01 +0300 Subject: [PATCH 177/516] FIXES: assistant run timeout (v10.9.2) - bugfix: long running assistant run now updates access tokens correctly --- common/version.go | 2 +- docs/changelog.md | 4 ++++ operations/authorize.go | 19 +++++++++++-------- operations/credentials.go | 3 ++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/common/version.go b/common/version.go index 8e8c0aed..628ce475 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.9.1` + Version = `v10.9.2` ) diff --git a/docs/changelog.md b/docs/changelog.md index 7014cc26..0dcd0b2d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.9.2 (date: 30.8.2021) + +- bugfix: long running assistant run now updates access tokens correctly + ## v10.9.1 (date: 27.8.2021) - made problems in assistant heartbeats visible diff --git a/operations/authorize.go b/operations/authorize.go index 05465f6b..f24e33cc 100644 --- a/operations/authorize.go +++ b/operations/authorize.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" @@ -173,13 +174,14 @@ func AuthorizeClaims(accountName string, claims *Claims) (Token, error) { } func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (Token, error) { + when := time.Now().Unix() found, ok := account.Cached(claims.Name, claims.Url) if ok { cached := make(Token) cached["endpoint"] = client.Endpoint() cached["requested"] = claims cached["status"] = "200" - cached["when"] = common.When + cached["when"] = when cached["token"] = found return cached, nil } @@ -190,7 +192,7 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To } bodyHash := Digest(body) size := len([]byte(body)) - nonce := fmt.Sprintf("%d", common.When) + nonce := fmt.Sprintf("%d", when) signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson @@ -210,11 +212,11 @@ func AuthorizeCommand(client cloud.Client, account *account, claims *Claims) (To token["endpoint"] = client.Endpoint() token["requested"] = claims token["status"] = response.Status - token["when"] = common.When - account.WasVerified(common.When) + token["when"] = when + account.WasVerified(when) trueToken, ok := token["token"].(string) if ok { - deadline := common.When + int64(3*(claims.ExpiresIn/4)) + deadline := when + int64(3*(claims.ExpiresIn/4)) account.CacheToken(claims.Name, claims.Url, trueToken, deadline) } return token, nil @@ -237,9 +239,10 @@ func DeleteAccount(client cloud.Client, account *account) error { } func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { + when := time.Now().Unix() claims := VerificationClaims() bodyHash := Digest("{}") - nonce := fmt.Sprintf("%d", common.When) + nonce := fmt.Sprintf("%d", when) signed := HmacSignature(claims, account.Secret, nonce, bodyHash) request := client.NewRequest(claims.Url) request.Headers[contentType] = applicationJson @@ -258,9 +261,9 @@ func UserinfoCommand(client cloud.Client, account *account) (*UserInfo, error) { link["endpoint"] = client.Endpoint() link["requested"] = claims link["status"] = response.Status - link["when"] = common.When + link["when"] = when result.Link = link - account.WasVerified(common.When) + account.WasVerified(when) account.UpdateDetails(result.User) return &result, nil } diff --git a/operations/credentials.go b/operations/credentials.go index 17afc3de..765edbc2 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -125,7 +125,8 @@ func (it *account) Cached(name, url string) (string, bool) { if !ok { return "", false } - if found.Deadline < common.When { + when := time.Now().Unix() + if found.Deadline < when { return "", false } common.Timeline("cached token: %s", name) From 3c837796d20e39c99bc9ba49981f04d7221041d0 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 31 Aug 2021 10:22:53 +0300 Subject: [PATCH 178/516] RCC-195: more diagnostics warnings (v10.9.3) - added diagnostic warnings on `PLAYWRIGHT_BROWSERS_PATH`, `NODE_OPTIONS`, and `NODE_PATH` environment variables when they are set --- common/version.go | 2 +- docs/changelog.md | 5 +++++ operations/diagnostics.go | 15 +++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/common/version.go b/common/version.go index 628ce475..e671ada6 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.9.2` + Version = `v10.9.3` ) diff --git a/docs/changelog.md b/docs/changelog.md index 0dcd0b2d..a4e277d7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v10.9.3 (date: 31.8.2021) + +- added diagnostic warnings on `PLAYWRIGHT_BROWSERS_PATH`, `NODE_OPTIONS`, + and `NODE_PATH` environment variables when they are set + ## v10.9.2 (date: 30.8.2021) - bugfix: long running assistant run now updates access tokens correctly diff --git a/operations/diagnostics.go b/operations/diagnostics.go index 9f6a5fc7..ab78f7d4 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -72,7 +72,10 @@ func RunDiagnostics() *common.DiagnosticStatus { // checks result.Checks = append(result.Checks, robocorpHomeCheck()) - result.Checks = append(result.Checks, pythonPathCheck()) + result.Checks = append(result.Checks, anyPathCheck("PYTHONPATH")) + result.Checks = append(result.Checks, anyPathCheck("PLAYWRIGHT_BROWSERS_PATH")) + result.Checks = append(result.Checks, anyPathCheck("NODE_OPTIONS")) + result.Checks = append(result.Checks, anyPathCheck("NODE_PATH")) if !common.OverrideSystemRequirements() { result.Checks = append(result.Checks, longPathSupportCheck()) } @@ -112,21 +115,21 @@ func longPathSupportCheck() *common.DiagnosticCheck { } } -func pythonPathCheck() *common.DiagnosticCheck { +func anyPathCheck(key string) *common.DiagnosticCheck { supportGeneralUrl := settings.Global.DocsLink("troubleshooting") - pythonPath := os.Getenv("PYTHONPATH") - if len(pythonPath) > 0 { + anyPath := os.Getenv(key) + if len(anyPath) > 0 { return &common.DiagnosticCheck{ Type: "OS", Status: statusWarning, - Message: fmt.Sprintf("PYTHONPATH is set to %q. This may cause problems.", pythonPath), + Message: fmt.Sprintf("%s is set to %q. This may cause problems.", key, anyPath), Link: supportGeneralUrl, } } return &common.DiagnosticCheck{ Type: "OS", Status: statusOk, - Message: "PYTHONPATH is not set, which is good.", + Message: fmt.Sprintf("%s is not set, which is good.", key), Link: supportGeneralUrl, } } From 48c038f2d2a1d0bb4398dc4ad98d7f7e4724631e Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 1 Sep 2021 16:30:04 +0300 Subject: [PATCH 179/516] RCC-169: holotree library check (v10.9.4) - invalidating hololib catalogs with broken files in hololib --- cmd/holotreeCheck.go | 23 ++++++++++++++++++++++- common/version.go | 2 +- docs/changelog.md | 4 ++++ htfs/functions.go | 25 ++++++++++++++++--------- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/cmd/holotreeCheck.go b/cmd/holotreeCheck.go index d92eef11..14e4a119 100644 --- a/cmd/holotreeCheck.go +++ b/cmd/holotreeCheck.go @@ -2,7 +2,9 @@ package cmd import ( "fmt" + "path/filepath" + "github.com/robocorp/rcc/anywork" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" @@ -26,10 +28,29 @@ func checkHolotreeIntegrity() { err = fs.Treetop(htfs.IntegrityCheck(collector)) common.Timeline("holotree integrity report") pretty.Guard(err == nil, 4, "%s", err) + purge := make(map[string]bool) for k, v := range collector { fmt.Println(k, v) + found, ok := known[filepath.Base(k)] + if !ok { + continue + } + for catalog, _ := range found { + purge[catalog] = true + } } - pretty.Guard(len(collector) == 0, 5, "Size: %d", len(collector)) + redo := false + for k, _ := range purge { + fmt.Println("Purge catalog:", k) + redo = true + anywork.Backlog(htfs.RemoveFile(k)) + } + if redo { + pretty.Warning("Some catalogs were purged. Run this check command again, please!") + } + err = anywork.Sync() + pretty.Guard(err == nil, 5, "%s", err) + pretty.Guard(len(collector) == 0, 6, "Size: %d", len(collector)) } var holotreeCheckCmd = &cobra.Command{ diff --git a/common/version.go b/common/version.go index e671ada6..7a3cbf9f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.9.3` + Version = `v10.9.4` ) diff --git a/docs/changelog.md b/docs/changelog.md index a4e277d7..ff78b5a5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # rcc change log +## v10.9.4 (date: 31.8.2021) + +- invalidating hololib catalogs with broken files in hololib + ## v10.9.3 (date: 31.8.2021) - added diagnostic warnings on `PLAYWRIGHT_BROWSERS_PATH`, `NODE_OPTIONS`, diff --git a/htfs/functions.go b/htfs/functions.go index cc6b9472..5ac0fc27 100644 --- a/htfs/functions.go +++ b/htfs/functions.go @@ -82,7 +82,7 @@ func IntegrityCheck(result map[string]string) Treetop { return tool } -func Hasher(known map[string]string) Filetask { +func Hasher(known map[string]map[string]bool) Filetask { return func(fullpath string, details *File) anywork.Work { return func() { _, ok := known[details.Name] @@ -378,18 +378,24 @@ func ZipRoot(library MutableLibrary, fs *Root, sink Zipper) Treetop { return tool } -func LoadHololibHashes() map[string]string { - roots := LoadCatalogs() +func LoadHololibHashes() map[string]map[string]bool { + catalogs, roots := LoadCatalogs() slots := make([]map[string]string, len(roots)) for at, root := range roots { anywork.Backlog(DigestLoader(root, at, slots)) } - result := make(map[string]string) + result := make(map[string]map[string]bool) runtime.Gosched() anywork.Sync() - for _, slot := range slots { - for k, v := range slot { - result[k] = v + for at, slot := range slots { + catalog := catalogs[at] + for k, _ := range slot { + found, ok := result[k] + if !ok { + found = make(map[string]bool) + result[k] = found + } + found[catalog] = true } } return result @@ -408,7 +414,7 @@ func DigestLoader(root *Root, at int, slots []map[string]string) anywork.Work { } } -func LoadCatalogs() []*Root { +func LoadCatalogs() ([]string, []*Root) { common.Timeline("catalog load start") defer common.Timeline("catalog load done") catalogs := Catalogs() @@ -416,10 +422,11 @@ func LoadCatalogs() []*Root { for at, catalog := range catalogs { fullpath := filepath.Join(common.HololibCatalogLocation(), catalog) anywork.Backlog(CatalogLoader(fullpath, at, roots)) + catalogs[at] = fullpath } runtime.Gosched() anywork.Sync() - return roots + return catalogs, roots } func CatalogLoader(catalog string, at int, roots []*Root) anywork.Work { From ed4ff66570bb6d7f8ed51faaa5fc6bef183a0929 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 6 Sep 2021 10:43:49 +0300 Subject: [PATCH 180/516] RCC-184: removal of base/live environments (v11.0.0) - BREAKING CHANGES (ongoing work, small steps, considered unstable) and goal is to remove old base/live environment handling and make holotree default and only way to manage environments - setting "user" as default space for all commands that need environments --- cmd/assistantRun.go | 2 +- cmd/cleanup.go | 1 - cmd/cloudPrepare.go | 2 +- cmd/env.go | 1 + cmd/envNew.go | 1 + cmd/holotreeDelete.go | 2 +- cmd/holotreeVariables.go | 3 +-- cmd/robotdependencies.go | 2 +- cmd/run.go | 2 +- cmd/script.go | 2 +- cmd/testrun.go | 2 +- common/version.go | 2 +- docs/changelog.md | 7 +++++++ robot_tests/exitcodes.robot | 2 -- robot_tests/fullrun.robot | 8 ++++---- robot_tests/resources.robot | 2 +- 16 files changed, 23 insertions(+), 18 deletions(-) diff --git a/cmd/assistantRun.go b/cmd/assistantRun.go index 7f3bc664..78ec6516 100644 --- a/cmd/assistantRun.go +++ b/cmd/assistantRun.go @@ -125,5 +125,5 @@ func init() { assistantRunCmd.MarkFlagRequired("assistant") assistantRunCmd.Flags().StringVarP(©Directory, "copy", "c", "", "Location to copy changed artifacts from run (optional).") assistantRunCmd.Flags().BoolVarP(&useEcc, "ecc", "", false, "DO NOT USE! INTERNAL EXPERIMENT!") - assistantRunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + assistantRunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") } diff --git a/cmd/cleanup.go b/cmd/cleanup.go index e56fd4bc..521faa86 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -36,7 +36,6 @@ After cleanup, they will not be available anymore.`, func init() { configureCmd.AddCommand(cleanupCmd) - envCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 82860ff5..84df1d17 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -72,5 +72,5 @@ func init() { prepareCloudCmd.MarkFlagRequired("workspace") prepareCloudCmd.Flags().StringVarP(&robotId, "robot", "r", "", "The robot id to use as the download source.") prepareCloudCmd.MarkFlagRequired("robot") - prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + prepareCloudCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") } diff --git a/cmd/env.go b/cmd/env.go index 5be410f8..e3db219d 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -15,6 +15,7 @@ used in task context locally.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { settings.CriticalEnvironmentSettingsCheck() }, + Hidden: true, } func init() { diff --git a/cmd/envNew.go b/cmd/envNew.go index 7072aab5..444e1625 100644 --- a/cmd/envNew.go +++ b/cmd/envNew.go @@ -16,6 +16,7 @@ When given multiple conda.yaml files, they will be merged together and the end result will be a composite environment.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + pretty.Exit(42, "DEPRECATED: env commands should not be used anymore!") if common.DebugFlag { defer common.Stopwatch("New environment creation lasted").Report() } diff --git a/cmd/holotreeDelete.go b/cmd/holotreeDelete.go index 7902d38d..d09b335f 100644 --- a/cmd/holotreeDelete.go +++ b/cmd/holotreeDelete.go @@ -41,5 +41,5 @@ var holotreeDeleteCmd = &cobra.Command{ func init() { holotreeCmd.AddCommand(holotreeDeleteCmd) holotreeDeleteCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") - holotreeDeleteCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify environment to delete.") + holotreeDeleteCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify environment to delete.") } diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 5795a6c2..40e36f03 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -98,8 +98,7 @@ func init() { holotreeVariablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") holotreeVariablesCmd.Flags().StringVarP(&accountName, "account", "a", "", "Account used for workspace. ") - holotreeVariablesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") - holotreeVariablesCmd.MarkFlagRequired("space") + holotreeVariablesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") holotreeVariablesCmd.Flags().BoolVarP(&holotreeForce, "force", "f", false, "Force environment creation with refresh.") holotreeVariablesCmd.Flags().BoolVarP(&holotreeJson, "json", "j", false, "Show environment as JSON.") } diff --git a/cmd/robotdependencies.go b/cmd/robotdependencies.go index cdffda3a..fe5f0b4e 100644 --- a/cmd/robotdependencies.go +++ b/cmd/robotdependencies.go @@ -59,5 +59,5 @@ func init() { robotDependenciesCmd.Flags().BoolVarP(&exportDependenciesFlag, "export", "e", false, "Export execution environment description into robot dependencies.yaml, overwriting previous if exists.") robotDependenciesCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Forced environment update.") robotDependenciesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") - robotDependenciesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Space to use for execution environment dependencies.") + robotDependenciesCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Space to use for execution environment dependencies.") } diff --git a/cmd/run.go b/cmd/run.go index 035b71cc..1f6e0ccd 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -57,6 +57,6 @@ func init() { runCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") runCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") runCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") - runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + runCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") runCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") } diff --git a/cmd/script.go b/cmd/script.go index 33484fb1..bc4092f5 100644 --- a/cmd/script.go +++ b/cmd/script.go @@ -44,5 +44,5 @@ func init() { scriptCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to the 'robot.yaml' configuration file.") scriptCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update (only for new environments).") scriptCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "", false, "Allow robot to be interactive in terminal/command prompt. For development only, not for production!") - scriptCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + scriptCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") } diff --git a/cmd/testrun.go b/cmd/testrun.go index 74662744..7ce9ded2 100644 --- a/cmd/testrun.go +++ b/cmd/testrun.go @@ -89,6 +89,6 @@ func init() { testrunCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. OPTIONAL") testrunCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. OPTIONAL") testrunCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") - testrunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "", "Client specific name to identify this environment.") + testrunCmd.Flags().StringVarP(&common.HolotreeSpace, "space", "s", "user", "Client specific name to identify this environment.") testrunCmd.Flags().BoolVarP(&common.NoOutputCapture, "no-outputs", "", false, "Do not capture stderr/stdout into files.") } diff --git a/common/version.go b/common/version.go index 7a3cbf9f..f04ce180 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v10.9.4` + Version = `v11.0.0` ) diff --git a/docs/changelog.md b/docs/changelog.md index ff78b5a5..3a056393 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.0.0 (date: 6.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, small steps, considered unstable) and goal + is to remove old base/live environment handling and make holotree default + and only way to manage environments +- setting "user" as default space for all commands that need environments + ## v10.9.4 (date: 31.8.2021) - invalidating hololib catalogs with broken files in hololib diff --git a/robot_tests/exitcodes.robot b/robot_tests/exitcodes.robot index 3f237db0..8666da92 100644 --- a/robot_tests/exitcodes.robot +++ b/robot_tests/exitcodes.robot @@ -15,7 +15,6 @@ Help for rcc cloud subcommand 0 build/rcc cloud -h --controller citest Help for rcc community subcommand 0 build/rcc community -h --controller citests Help for rcc configure subcommand 0 build/rcc configure -h --controller citests Help for rcc create subcommand 0 build/rcc create -h --controller citests -Help for rcc env subcommand 0 build/rcc env -h --controller citests Help for rcc feedback subcommand 0 build/rcc feedback -h --controller citests Help for rcc holotree subcommand 0 build/rcc holotree -h --controller citests Help for rcc help subcommand 0 build/rcc help -h --controller citests @@ -34,7 +33,6 @@ Run rcc docs changelog 0 build/rcc docs changelog --controller Run rcc docs license 0 build/rcc docs license --controller citests Run rcc docs recipes 0 build/rcc docs recipes --controller citests Run rcc docs tutorial 0 build/rcc docs tutorial --controller citests -Run rcc environment list 0 build/rcc environment list --controller citests Run rcc holotree list 0 build/rcc holotree list --controller citests Run rcc tutorial 0 build/rcc tutorial --controller citests Run rcc version 0 build/rcc version --controller citests diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index cec6e217..ba563c8f 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -7,7 +7,7 @@ Resource resources.robot Goal: Show rcc version information. Step build/rcc version --controller citests - Must Have v10. + Must Have v11. Goal: Show rcc license information. Step build/rcc man license --controller citests @@ -112,13 +112,13 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Must Have Progress: 0/6 - Must Have Progress: 1/6 + Wont Have Progress: 0/6 + Wont Have Progress: 1/6 Wont Have Progress: 2/6 Wont Have Progress: 3/6 Wont Have Progress: 4/6 Wont Have Progress: 5/6 - Must Have Progress: 6/6 + Wont Have Progress: 6/6 Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 57f5d065..9f31f6bd 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -16,7 +16,7 @@ Prepare Local Set Environment Variable ROBOCORP_HOME tmp/robocorp Comment Verify micromamba is installed or download and install it. - Step build/rcc env new robot_tests/conda.yaml + Step build/rcc ht vars robot_tests/conda.yaml Must Exist %{ROBOCORP_HOME}/bin/ Must Exist %{ROBOCORP_HOME}/base/ Must Exist %{ROBOCORP_HOME}/live/ From be503c47e7056a6b806103009b089673d0525f40 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Sep 2021 09:59:56 +0300 Subject: [PATCH 181/516] RCC-184: removal of base/live environments (v11.0.1) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - fixing robot tests --- cmd/cleanup.go | 2 +- common/version.go | 2 +- docs/changelog.md | 5 +++++ htfs/commands.go | 1 + robot_tests/fullrun.robot | 26 ++++++++++++++------------ robot_tests/holotree.robot | 1 - 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 521faa86..e0315600 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -40,7 +40,7 @@ func init() { cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") - cleanupCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Cleanup all enviroments.") + cleanupCmd.Flags().BoolVarP(&allFlag, "all", "", false, "Cleanup all enviroments.") cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") cleanupCmd.Flags().IntVarP(&daysOption, "days", "", 30, "What is the limit in days to keep environments for (deletes environments older than this).") } diff --git a/common/version.go b/common/version.go index f04ce180..c62b0e4f 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.0` + Version = `v11.0.1` ) diff --git a/docs/changelog.md b/docs/changelog.md index 3a056393..14607e63 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.0.1 (date: 7.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- fixing robot tests + ## v11.0.0 (date: 6.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, small steps, considered unstable) and goal diff --git a/htfs/commands.go b/htfs/commands.go index 40aff09c..33091e90 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -58,6 +58,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) + common.EnvironmentHash = BlueprintHash(holotreeBlueprint) return path, nil } diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index ba563c8f..58cfda0b 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -122,21 +122,23 @@ Goal: Run task in clean temporary directory. Must Have OK. Goal: Merge two different conda.yaml files with conflict fails - Step build/rcc env new --controller citests conda/testdata/conda.yaml conda/testdata/other.yaml 1 + Step build/rcc holotree vars --controller citests conda/testdata/conda.yaml conda/testdata/other.yaml 5 Use STDERR Must Have robotframework=3.1 vs. robotframework=3.2 Goal: Merge two different conda.yaml files without conflict passes - Step build/rcc env new --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent - Must Have 0cc761cfb9692a36 + Step build/rcc holotree vars --controller citests conda/testdata/other.yaml conda/testdata/third.yaml --silent + Must Have 5bea0c1d2419493e + Must Have 4e67cd8d4_9fcd2534 Goal: Can list environments as JSON - Step build/rcc env list --controller citests --json - Must Have 0cc761cfb9692a36 + Step build/rcc holotree list --controller citests --json + Must Have 4e67cd8d4_9fcd2534 + Must Have 5bea0c1d2419493e Must Be Json Response Goal: See variables from specific environment without robot.yaml knowledge - Step build/rcc env variables --controller citests conda/testdata/conda.yaml + Step build/rcc holotree variables --controller citests conda/testdata/conda.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc @@ -155,10 +157,10 @@ Goal: See variables from specific environment without robot.yaml knowledge Wont Have PYTHONPATH= Wont Have ROBOT_ROOT= Wont Have ROBOT_ARTIFACTS= - Must Have f0a9e281269b31ea + Must Have 54399f4561ae95af Goal: See variables from specific environment with robot.yaml but without task - Step build/rcc env variables --controller citests -r tmp/fluffy/robot.yaml + Step build/rcc holotree variables --controller citests -r tmp/fluffy/robot.yaml Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc @@ -171,7 +173,7 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= - Must Have RCC_ENVIRONMENT_HASH=199e494e4c733ef3 + Must Have RCC_ENVIRONMENT_HASH=2e3ef3ffef58c9ec Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= Must Have PYTHONPATH= @@ -179,11 +181,11 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have ROBOT_ARTIFACTS= Goal: See variables from specific environment without robot.yaml knowledge in JSON form - Step build/rcc env variables --controller citests --json conda/testdata/conda.yaml + Step build/rcc holotree variables --controller citests --json conda/testdata/conda.yaml Must Be Json Response Goal: See variables from specific environment with robot.yaml knowledge - Step build/rcc env variables --task "Run Example task" --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json + Step build/rcc holotree variables --controller citests conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Have ROBOCORP_HOME= Must Have PYTHON_EXE= Must Have CONDA_DEFAULT_ENV=rcc @@ -209,7 +211,7 @@ Goal: See variables from specific environment with robot.yaml knowledge Wont Have RC_WORKSPACE_ID= Goal: See variables from specific environment with robot.yaml knowledge in JSON form - Step build/rcc env variables --task "Run Example task" --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json + Step build/rcc holotree variables --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/fluffy/robot.yaml -e tmp/fluffy/devdata/env.json Must Be Json Response Goal: See diagnostics as valid JSON form diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index ed63857d..3a2ab82c 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -83,7 +83,6 @@ Goal: See variables from specific environment with robot.yaml knowledge Wont Have RC_WORKSPACE_ID= Use STDERR Wont Have (virtual) - Must Have live only Goal: See variables from specific environment with robot.yaml knowledge in JSON form Step build/rcc holotree variables --space jam --controller citests --json conda/testdata/conda.yaml --config tmp/alternative.yaml -r tmp/holotin/robot.yaml -e tmp/holotin/devdata/env.json From 98e324b934ca64308e9c8cbe1cace49cdc06723b Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 7 Sep 2021 15:39:11 +0300 Subject: [PATCH 182/516] RCC-196: cleanup improvements (v11.0.2) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - technical work: cherry-picking changes from v10.10.0 into series 11 - rcc config cleanup improvement, so that not partial cleanup is done on holotree structure (on Windows, respecting locked environments) --- .github/workflows/rcc.yaml | 1 + common/version.go | 2 +- conda/cleanup.go | 34 ++++++++++++++++++++-------------- conda/workflows.go | 22 +++++++++++----------- docs/changelog.md | 11 +++++++++++ 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/.github/workflows/rcc.yaml b/.github/workflows/rcc.yaml index a5f4693a..df19057a 100644 --- a/.github/workflows/rcc.yaml +++ b/.github/workflows/rcc.yaml @@ -6,6 +6,7 @@ on: branches: - master - maintenance + - series10 jobs: build: diff --git a/common/version.go b/common/version.go index c62b0e4f..52e110dd 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.1` + Version = `v11.0.2` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 954826e7..0d5d07b0 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -10,22 +10,24 @@ import ( "github.com/robocorp/rcc/pretty" ) -func safeRemove(hint, pathling string) { +func safeRemove(hint, pathling string) error { var err error if !pathlib.Exists(pathling) { common.Debug("[%s] Missing %v, not need to remove.", hint, pathling) - return + return nil } if pathlib.IsDir(pathling) { - err = os.RemoveAll(pathling) + err = renameRemove(pathling) } else { err = os.Remove(pathling) } if err != nil { pretty.Warning("[%s] %s -> %v", hint, pathling, err) + pretty.Warning("Make sure that you have rights to %q, and that nothing has locks in there.", pathling) } else { common.Debug("[%s] Removed %v.", hint, pathling) } + return err } func doCleanup(fullpath string, dryrun bool) error { @@ -62,33 +64,37 @@ func quickCleanup(dryrun bool) error { common.Log("Would be removing:") common.Log("- %v", common.BaseLocation()) common.Log("- %v", common.LiveLocation()) - common.Log("- %v", common.HolotreeLocation()) + common.Log("- %v", MinicondaLocation()) common.Log("- %v", common.PipCache()) + common.Log("- %v", common.HolotreeLocation()) common.Log("- %v", RobocorpTempRoot()) - common.Log("- %v", MinicondaLocation()) return nil } - safeRemove("cache", common.HolotreeLocation()) safeRemove("cache", common.BaseLocation()) safeRemove("cache", common.LiveLocation()) - safeRemove("cache", common.PipCache()) - safeRemove("temp", RobocorpTempRoot()) safeRemove("cache", MinicondaLocation()) - return nil + safeRemove("cache", common.PipCache()) + err := safeRemove("cache", common.HolotreeLocation()) + if err != nil { + return err + } + return safeRemove("temp", RobocorpTempRoot()) } func spotlessCleanup(dryrun bool) error { - quickCleanup(dryrun) + err := quickCleanup(dryrun) + if err != nil { + return err + } if dryrun { + common.Log("- %v", MambaPackages()) common.Log("- %v", BinMicromamba()) common.Log("- %v", common.HololibLocation()) - common.Log("- %v", MambaPackages()) return nil } - safeRemove("executable", BinMicromamba()) - safeRemove("cache", common.HololibLocation()) safeRemove("cache", MambaPackages()) - return nil + safeRemove("executable", BinMicromamba()) + return safeRemove("cache", common.HololibLocation()) } func cleanupTemp(deadline time.Time, dryrun bool) error { diff --git a/conda/workflows.go b/conda/workflows.go index 1158f80c..a01d430f 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -80,7 +80,7 @@ func reuseExistingLive(key string) (bool, error) { touchMetafile(candidate) return true, nil } - err := removeClone(candidate) + err := renameRemove(candidate) if err != nil { return false, err } @@ -137,7 +137,7 @@ func (it InstallObserver) Write(content []byte) (int, error) { func (it InstallObserver) HasFailures(targetFolder string) bool { if it["safetyerror"] && it["corrupted"] && len(it) > 2 { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.creation.failure", common.Version) - removeClone(targetFolder) + renameRemove(targetFolder) location := filepath.Join(common.RobocorpHome(), "pkgs") common.Log("%sWARNING! Conda environment is unstable, see above error.%s", pretty.Red, pretty.Reset) common.Log("%sWARNING! To fix it, try to remove directory: %v%s", pretty.Red, location, pretty.Reset) @@ -153,7 +153,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall targetFolder := LiveFrom(key) common.Debug("=== new live --- pre cleanup phase ===") common.Timeline("pre cleanup phase.") - err := removeClone(targetFolder) + err := renameRemove(targetFolder) if err != nil { return false, err } @@ -166,7 +166,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall common.Timeline("second try.") common.ForceDebug() common.Log("Retry! First try failed ... now retrying with debug and force options!") - err = removeClone(targetFolder) + err = renameRemove(targetFolder) if err != nil { return false, err } @@ -486,14 +486,14 @@ func FindEnvironment(prefix string) []string { } func RemoveEnvironment(label string) error { - err := removeClone(LiveFrom(label)) + err := renameRemove(LiveFrom(label)) if err != nil { return err } - return removeClone(TemplateFrom(label)) + return renameRemove(TemplateFrom(label)) } -func removeClone(location string) error { +func renameRemove(location string) error { if !pathlib.IsDir(location) { common.Trace("Location %q is not directory, not removed.", location) return nil @@ -525,7 +525,7 @@ func removeClone(location string) error { func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { common.Trace("Start CloneFromTo: %q -> %q", source, target) defer common.Trace("Done CloneFromTo: %q -> %q", source, target) - err := removeClone(target) + err := renameRemove(target) if err != nil { return false, err } @@ -535,7 +535,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { if common.Liveonly { common.Debug("Clone source %q is dirty, but wont remove since --liveonly flag.", source) } else { - err = removeClone(source) + err = renameRemove(source) if err != nil { return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) } @@ -550,7 +550,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { } success := cloneFolder(source, target, 100, copier) if !success { - err = removeClone(target) + err = renameRemove(target) if err != nil { return false, fmt.Errorf("Cloning %q to %q failed! And cleanup failed: %v", source, target, err) } @@ -565,7 +565,7 @@ func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { created := make(map[string]string) createdHash, createdErr := DigestFor(target, created) DiagnoseDirty(source, target, originHash, createdHash, originErr, createdErr, origin, created, false) - err = removeClone(target) + err = renameRemove(target) if err != nil { return false, fmt.Errorf("Target %q does not match source %q! And cleanup failed: %v!", target, source, err) } diff --git a/docs/changelog.md b/docs/changelog.md index 14607e63..a423485a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # rcc change log +## v11.0.2 (date: 8.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- technical work: cherry-picking changes from v10.10.0 into series 11 + ## v11.0.1 (date: 7.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) @@ -12,6 +17,12 @@ and only way to manage environments - setting "user" as default space for all commands that need environments +## v10.10.0 (date: 7.9.2021) + +- this is series 10 maitenance branch +- rcc config cleanup improvement, so that not partial cleanup is done on + holotree structure (on Windows, respecting locked environments) + ## v10.9.4 (date: 31.8.2021) - invalidating hololib catalogs with broken files in hololib From 25442158bbb66ee53f2088bb97fde0f755e1f07d Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 8 Sep 2021 16:10:24 +0300 Subject: [PATCH 183/516] RCC-184: removal of base/live environments (v11.0.3) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - removed commands "new", "delete", "list", and "variables" from `rcc env` command set - replaced `rcc env hash` with new `rcc holotree hash`, which now calculates blueprint fingerprint hash similar way that env hash but differently because holotree uses siphash algorithm --- cmd/cloudPrepare.go | 7 +- cmd/delete.go | 30 --------- cmd/envHash.go | 34 ---------- cmd/envNew.go | 38 ----------- cmd/holotreeHash.go | 32 +++++++++ cmd/holotreeVariables.go | 41 ++++++++++++ cmd/list.go | 81 ---------------------- cmd/variables.go | 140 --------------------------------------- common/version.go | 2 +- conda/cleanup.go | 1 + conda/workflows.go | 1 + docs/changelog.md | 9 +++ operations/running.go | 7 +- 13 files changed, 87 insertions(+), 336 deletions(-) delete mode 100644 cmd/delete.go delete mode 100644 cmd/envHash.go delete mode 100644 cmd/envNew.go create mode 100644 cmd/holotreeHash.go delete mode 100644 cmd/list.go delete mode 100644 cmd/variables.go diff --git a/cmd/cloudPrepare.go b/cmd/cloudPrepare.go index 84df1d17..f1ff0864 100644 --- a/cmd/cloudPrepare.go +++ b/cmd/cloudPrepare.go @@ -7,7 +7,6 @@ import ( "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pathlib" @@ -54,11 +53,7 @@ var prepareCloudCmd = &cobra.Command{ var label string condafile := config.CondaConfigFile() - if len(common.HolotreeSpace) > 0 { - label, err = htfs.NewEnvironment(false, condafile, config.Holozip()) - } else { - label, err = conda.NewEnvironment(false, condafile) - } + label, err = htfs.NewEnvironment(false, condafile, config.Holozip()) pretty.Guard(err == nil, 8, "Error: %v", err) common.Log("Prepared %q.", label) diff --git a/cmd/delete.go b/cmd/delete.go deleted file mode 100644 index 2aad1171..00000000 --- a/cmd/delete.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var deleteCmd = &cobra.Command{ - Use: "delete +", - Short: "Delete one managed virtual environment.", - Long: `Delete the given virtual environment from existence. -After deletion, it will not be available anymore.`, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - for _, prefix := range args { - for _, label := range conda.FindEnvironment(prefix) { - common.Log("Removing %v", label) - err := conda.RemoveEnvironment(label) - pretty.Guard(err == nil, 1, "Error: %v", err) - } - } - }, -} - -func init() { - envCmd.AddCommand(deleteCmd) -} diff --git a/cmd/envHash.go b/cmd/envHash.go deleted file mode 100644 index b46b2665..00000000 --- a/cmd/envHash.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var envHashCmd = &cobra.Command{ - Use: "hash ", - Short: "Calculates a hash for managed virtual environment from conda.yaml files.", - Long: "Calculates a hash for managed virtual environment from conda.yaml files.", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - if common.DebugFlag { - defer common.Stopwatch("Conda YAML hash calculation lasted").Report() - } - hash, err := conda.CalculateComboHash(args...) - if err != nil { - pretty.Exit(1, "Hash calculation failed: %v", err) - } else { - common.Log("Hash for %v is %v.", args, hash) - } - if common.Silent { - common.Stdout("%s\n", hash) - } - }, -} - -func init() { - envCmd.AddCommand(envHashCmd) -} diff --git a/cmd/envNew.go b/cmd/envNew.go deleted file mode 100644 index 444e1625..00000000 --- a/cmd/envNew.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var newEnvCmd = &cobra.Command{ - Use: "new ", - Short: "Creates a new managed virtual environment.", - Long: `The new command can be used to create a new managed virtual environment. -When given multiple conda.yaml files, they will be merged together and the -end result will be a composite environment.`, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - pretty.Exit(42, "DEPRECATED: env commands should not be used anymore!") - if common.DebugFlag { - defer common.Stopwatch("New environment creation lasted").Report() - } - label, err := conda.NewEnvironment(forceFlag, args...) - if err != nil { - pretty.Exit(1, "Environment creation failed: %v", err) - } else { - common.Log("Environment for %v as %v created.", args, label) - } - if common.Silent { - common.Stdout("%s\n", label) - } - }, -} - -func init() { - envCmd.AddCommand(newEnvCmd) - newEnvCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") -} diff --git a/cmd/holotreeHash.go b/cmd/holotreeHash.go new file mode 100644 index 00000000..0e4ce8d7 --- /dev/null +++ b/cmd/holotreeHash.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/htfs" + "github.com/robocorp/rcc/pretty" + + "github.com/spf13/cobra" +) + +var holotreeHashCmd = &cobra.Command{ + Use: "hash ", + Short: "Calculates a blueprint hash for managed holotree virtual environment from conda.yaml files.", + Long: "Calculates a blueprint hash for managed holotree virtual environment from conda.yaml files.", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if common.DebugFlag { + defer common.Stopwatch("Conda YAML hash calculation lasted").Report() + } + _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(args, "") + pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) + hash := htfs.BlueprintHash(holotreeBlueprint) + common.Log("Blueprint hash for %v is %v.", args, hash) + if common.Silent { + common.Stdout("%s\n", hash) + } + }, +} + +func init() { + holotreeCmd.AddCommand(holotreeHashCmd) +} diff --git a/cmd/holotreeVariables.go b/cmd/holotreeVariables.go index 40e36f03..3daa02cf 100644 --- a/cmd/holotreeVariables.go +++ b/cmd/holotreeVariables.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/conda" @@ -20,6 +21,46 @@ var ( holotreeJson bool ) +func asSimpleMap(line string) map[string]string { + parts := strings.SplitN(strings.TrimSpace(line), "=", 2) + if len(parts) != 2 { + return nil + } + if len(parts[0]) == 0 { + return nil + } + result := make(map[string]string) + result["key"] = parts[0] + result["value"] = parts[1] + return result +} + +func asJson(items []string) error { + result := make([]map[string]string, 0, len(items)) + for _, line := range items { + entry := asSimpleMap(line) + if entry != nil { + result = append(result, entry) + } + } + content, err := operations.NiceJsonOutput(result) + if err != nil { + return err + } + common.Stdout("%s\n", content) + return nil +} + +func asExportedText(items []string) { + prefix := "export" + if conda.IsWindows() { + prefix = "SET" + } + for _, line := range items { + common.Stdout("%s %s\n", prefix, line) + } +} + func holotreeExpandEnvironment(userFiles []string, packfile, environment, workspace string, validity int, force bool) []string { var extra []string var data operations.Token diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index 56e3eb2b..00000000 --- a/cmd/list.go +++ /dev/null @@ -1,81 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "sort" - "time" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -func textDump(lines []string) { - sort.Strings(lines) - for _, line := range lines { - common.Log("%s", line) - } -} - -func jsonDump(entries map[string]interface{}) { - body, err := json.MarshalIndent(entries, "", " ") - if err == nil { - fmt.Fprintln(os.Stdout, string(body)) - } -} - -var listCmd = &cobra.Command{ - Use: "list", - Short: "Listing currently managed virtual environments.", - Long: `List shows listing of currently managed virtual environments -in human readable form.`, - Run: func(cmd *cobra.Command, args []string) { - templates := conda.TemplateList() - if len(templates) == 0 { - pretty.Exit(1, "No environments available.") - } - lines := make([]string, 0, len(templates)) - entries := make(map[string]interface{}) - if !jsonFlag { - common.Log("%-25s %-25s %-16s %5s", "Last used", "Last cloned", "Environment", "Plan?") - } - for _, template := range templates { - details := make(map[string]interface{}) - entries[template] = details - cloned := "N/A" - used := cloned - when, err := conda.LastUsed(conda.TemplateFrom(template)) - if err == nil { - cloned = when.Format(time.RFC3339) - } - when, err = conda.LastUsed(conda.LiveFrom(template)) - if err == nil { - used = when.Format(time.RFC3339) - } - details["name"] = template - details["used"] = used - details["cloned"] = cloned - details["base"] = conda.TemplateFrom(template) - details["live"] = conda.LiveFrom(template) - planfile, plan := conda.InstallationPlan(template) - lines = append(lines, fmt.Sprintf("%-25s %-25s %-16s %5v", used, cloned, template, plan)) - if plan { - details["plan"] = planfile - } - } - if jsonFlag { - jsonDump(entries) - } else { - textDump(lines) - } - }, -} - -func init() { - envCmd.AddCommand(listCmd) - listCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format.") -} diff --git a/cmd/variables.go b/cmd/variables.go deleted file mode 100644 index 43115a1e..00000000 --- a/cmd/variables.go +++ /dev/null @@ -1,140 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "strings" - - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/htfs" - "github.com/robocorp/rcc/operations" - "github.com/robocorp/rcc/pretty" - "github.com/robocorp/rcc/robot" - - "github.com/spf13/cobra" -) - -func asSimpleMap(line string) map[string]string { - parts := strings.SplitN(strings.TrimSpace(line), "=", 2) - if len(parts) != 2 { - return nil - } - if len(parts[0]) == 0 { - return nil - } - result := make(map[string]string) - result["key"] = parts[0] - result["value"] = parts[1] - return result -} - -func asJson(items []string) error { - result := make([]map[string]string, 0, len(items)) - for _, line := range items { - entry := asSimpleMap(line) - if entry != nil { - result = append(result, entry) - } - } - content, err := operations.NiceJsonOutput(result) - if err != nil { - return err - } - common.Stdout("%s\n", content) - return nil -} - -func asExportedText(items []string) { - prefix := "export" - if conda.IsWindows() { - prefix = "SET" - } - for _, line := range items { - common.Stdout("%s %s\n", prefix, line) - } -} - -func exportEnvironment(userCondaYaml []string, packfile, environment, workspace string, validity int, jsonform bool) error { - var err error - var extra []string - var data operations.Token - - config, condaYaml := htfs.RobotBlueprints(userCondaYaml, packfile) - - if Has(environment) { - developmentEnvironment, err := robot.LoadEnvironmentSetup(environment) - if err == nil { - extra = developmentEnvironment.AsEnvironment() - } - } - - if len(condaYaml) < 1 { - return errors.New("No robot.yaml, or conda.yaml files given. Cannot continue.") - } - - label, err := conda.NewEnvironment(forceFlag, condaYaml...) - if err != nil { - return err - } - - env := conda.EnvironmentExtensionFor(label) - if config != nil { - env = config.ExecutionEnvironment(label, extra, false) - } - - if Has(workspace) { - claims := operations.RunRobotClaims(validity*60, workspace) - data, err = operations.AuthorizeClaims(AccountName(), claims) - } - - if err != nil { - return err - } - - if len(data) > 0 { - endpoint := data["endpoint"] - for _, key := range rcHosts { - env = append(env, fmt.Sprintf("%s=%s", key, endpoint)) - } - token := data["token"] - for _, key := range rcTokens { - env = append(env, fmt.Sprintf("%s=%s", key, token)) - } - env = append(env, fmt.Sprintf("RC_WORKSPACE_ID=%s", workspaceId)) - } - - if jsonform { - return asJson(env) - } - - asExportedText(env) - return nil -} - -var variablesCmd = &cobra.Command{ - Use: "variables ", - Aliases: []string{"vars"}, - Short: "Export environment specific variables as a JSON structure.", - Long: "Export environment specific variables as a JSON structure.", - Run: func(cmd *cobra.Command, args []string) { - err := exportEnvironment(args, robotFile, environmentFile, workspaceId, validityTime, jsonFlag) - if err != nil { - pretty.Exit(1, "Error: Variable exporting failed because: %v", err) - } - }, -} - -func init() { - envCmd.AddCommand(variablesCmd) - - variablesCmd.Flags().StringVarP(&environmentFile, "environment", "e", "", "Full path to 'env.json' development environment data file. ") - variablesCmd.Flags().StringVarP(&robotFile, "robot", "r", "robot.yaml", "Full path to 'robot.yaml' configuration file. ") - variablesCmd.Flags().StringVarP(&runTask, "task", "t", "", "Task to run from configuration file. ") - variablesCmd.Flags().StringVarP(&workspaceId, "workspace", "w", "", "Optional workspace id to get authorization tokens for. ") - variablesCmd.Flags().IntVarP(&validityTime, "minutes", "m", 0, "How many minutes the authorization should be valid for. ") - variablesCmd.Flags().StringVarP(&accountName, "account", "", "", "Account used for workspace. ") - - variablesCmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output in JSON format") - variablesCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Force conda cache update. (only for new environments)") -} diff --git a/common/version.go b/common/version.go index 52e110dd..5ee06e51 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.2` + Version = `v11.0.3` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 0d5d07b0..9ab515df 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -168,6 +168,7 @@ func Cleanup(daylimit int, dryrun, orphans, quick, all, miniconda, micromamba bo common.Log("Would be removing %v.", template) continue } + // FIXME: remove this when base/live removal is done RemoveEnvironment(template) common.Debug("Removed environment %v.", template) } diff --git a/conda/workflows.go b/conda/workflows.go index a01d430f..54d6016e 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -343,6 +343,7 @@ func ShortDigest(content string) string { } func CalculateComboHash(configurations ...string) (string, error) { + // FIXME: Remove this func once live/base is erased from codebase key, _, _, err := temporaryConfig("/dev/null", "/dev/null", false, configurations...) if err != nil { return "", err diff --git a/docs/changelog.md b/docs/changelog.md index a423485a..cbc3ca90 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.0.3 (date: 8.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- removed commands "new", "delete", "list", and "variables" from `rcc env` + command set +- replaced `rcc env hash` with new `rcc holotree hash`, which now calculates + blueprint fingerprint hash similar way that env hash but differently + because holotree uses siphash algorithm + ## v11.0.2 (date: 8.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/operations/running.go b/operations/running.go index cdce6ea4..42fa548b 100644 --- a/operations/running.go +++ b/operations/running.go @@ -114,12 +114,7 @@ func LoadTaskWithEnvironment(packfile, theTask string, force bool) (bool, robot. return true, config, todo, "" } - var label string - if len(common.HolotreeSpace) > 0 { - label, err = htfs.NewEnvironment(force, config.CondaConfigFile(), config.Holozip()) - } else { - label, err = conda.NewEnvironment(force, config.CondaConfigFile()) - } + label, err := htfs.NewEnvironment(force, config.CondaConfigFile(), config.Holozip()) if err != nil { pretty.Exit(4, "Error: %v", err) } From 160ee50b923fbb6e0b5426d1fc4bb9c1d8f1d7f7 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 9 Sep 2021 12:07:57 +0300 Subject: [PATCH 184/516] RCC-184: removal of base/live environments (v11.0.4) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - replaced `rcc env plan` with new `rcc holotree plan`, which now shows installation plans from holotree spaces - now all env commands are removed, so also toplevel "env" command is gone - added naive helper script, deadcode.py, to find dead code - cleaned up some dead code branches --- cmd/env.go | 25 --------------- cmd/holotree.go | 4 +++ cmd/{envPlan.go => holotreePlan.go} | 14 ++++---- common/version.go | 2 +- conda/platform_windows_amd64.go | 4 --- conda/robocorp.go | 8 ----- conda/workflows.go | 30 ----------------- docs/changelog.md | 9 ++++++ htfs/commands.go | 5 +++ operations/assistant.go | 13 -------- operations/credentials.go | 6 ---- operations/workspaces.go | 29 ----------------- scripts/deadcode.py | 50 +++++++++++++++++++++++++++++ wizard/common.go | 17 ---------- 14 files changed, 76 insertions(+), 140 deletions(-) delete mode 100644 cmd/env.go rename cmd/{envPlan.go => holotreePlan.go} (56%) create mode 100755 scripts/deadcode.py diff --git a/cmd/env.go b/cmd/env.go deleted file mode 100644 index e3db219d..00000000 --- a/cmd/env.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/settings" - "github.com/spf13/cobra" -) - -var envCmd = &cobra.Command{ - Use: "env", - Aliases: []string{"environment", "e"}, - Short: "Group of commands related to `environment management`.", - Long: `This "env" command set is for managing virtual environments, -used in task context locally.`, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - settings.CriticalEnvironmentSettingsCheck() - }, - Hidden: true, -} - -func init() { - - rootCmd.AddCommand(envCmd) - envCmd.PersistentFlags().StringVar(&common.StageFolder, "stage", "", "internal, DO NOT USE (unless you know what you are doing)") -} diff --git a/cmd/holotree.go b/cmd/holotree.go index 32821b3b..902259e9 100644 --- a/cmd/holotree.go +++ b/cmd/holotree.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/robocorp/rcc/settings" "github.com/spf13/cobra" ) @@ -9,6 +10,9 @@ var holotreeCmd = &cobra.Command{ Aliases: []string{"ht"}, Short: "Group of holotree commands.", Long: "Group of holotree commands.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + settings.CriticalEnvironmentSettingsCheck() + }, } func init() { diff --git a/cmd/envPlan.go b/cmd/holotreePlan.go similarity index 56% rename from cmd/envPlan.go rename to cmd/holotreePlan.go index 270c6eaa..520e932d 100644 --- a/cmd/envPlan.go +++ b/cmd/holotreePlan.go @@ -5,21 +5,21 @@ import ( "io/ioutil" "os" - "github.com/robocorp/rcc/conda" + "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/pretty" "github.com/spf13/cobra" ) -var envPlanCmd = &cobra.Command{ +var holotreePlanCmd = &cobra.Command{ Use: "plan", - Short: "Show installation plan for given environment (or prefix)", - Long: "Show installation plan for given environment (or prefix)", + Short: "Show installation plans for given holotree spaces (or substrings)", + Long: "Show installation plans for given holotree spaces (or substrings)", Run: func(cmd *cobra.Command, args []string) { for _, prefix := range args { - for _, label := range conda.FindEnvironment(prefix) { - planfile, ok := conda.InstallationPlan(label) + for _, label := range htfs.FindEnvironment(prefix) { + planfile, ok := htfs.InstallationPlan(label) pretty.Guard(ok, 1, "Could not find plan for: %v", label) content, err := ioutil.ReadFile(planfile) pretty.Guard(err == nil, 2, "Could not read plan %q, reason: %v", planfile, err) @@ -30,5 +30,5 @@ var envPlanCmd = &cobra.Command{ } func init() { - envCmd.AddCommand(envPlanCmd) + holotreeCmd.AddCommand(holotreePlanCmd) } diff --git a/common/version.go b/common/version.go index 5ee06e51..55b50d57 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.3` + Version = `v11.0.4` ) diff --git a/conda/platform_windows_amd64.go b/conda/platform_windows_amd64.go index fb8a76c5..8c00f0ce 100644 --- a/conda/platform_windows_amd64.go +++ b/conda/platform_windows_amd64.go @@ -36,10 +36,6 @@ var ( FileExtensions = []string{".exe", ".com", ".bat", ".cmd", ""} ) -func ensureHardlinkEnvironmment() (string, error) { - return "", fmt.Errorf("Not implemented yet!") -} - func CondaEnvironment() []string { env := os.Environ() env = append(env, fmt.Sprintf("MAMBA_ROOT_PREFIX=%s", common.RobocorpHome())) diff --git a/conda/robocorp.go b/conda/robocorp.go index 5b8dfd12..83ebc290 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -192,10 +192,6 @@ func MambaPackages() string { return common.ExpandPath(filepath.Join(common.RobocorpHome(), "pkgs")) } -func MambaCache() string { - return common.ExpandPath(filepath.Join(MambaPackages(), "cache")) -} - func asVersion(text string) (uint64, string) { text = strings.TrimSpace(text) multiline := strings.SplitN(text, "\n", 2) @@ -285,10 +281,6 @@ func TemplateList() []string { return dirnamesFrom(common.BaseLocation()) } -func LiveList() []string { - return dirnamesFrom(common.LiveLocation()) -} - func OrphanList() []string { result := orphansFrom(common.BaseLocation()) result = append(result, orphansFrom(common.LiveLocation())...) diff --git a/conda/workflows.go b/conda/workflows.go index 54d6016e..7cb43217 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -474,18 +474,6 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return "", errors.New("Could not create environment.") } -func FindEnvironment(prefix string) []string { - prefix = strings.ToLower(prefix) - livelist := LiveList() - result := make([]string, 0, len(livelist)) - for _, entry := range livelist { - if strings.HasPrefix(entry, prefix) { - result = append(result, entry) - } - } - return result -} - func RemoveEnvironment(label string) error { err := renameRemove(LiveFrom(label)) if err != nil { @@ -597,24 +585,6 @@ func cloneFolder(source, target string, workers int, copier pathlib.Copier) bool return success } -func SilentTouch(directory string, when time.Time) bool { - handle, err := os.Open(directory) - if err != nil { - return false - } - entries, err := handle.Readdir(-1) - handle.Close() - if err != nil { - return false - } - for _, entry := range entries { - if !entry.IsDir() { - pathlib.TouchWhen(filepath.Join(directory, entry.Name()), when) - } - } - return true -} - func copyFolder(source, target string, queue chan copyRequest) bool { os.Mkdir(target, 0755) diff --git a/docs/changelog.md b/docs/changelog.md index cbc3ca90..bbe8d358 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,14 @@ # rcc change log +## v11.0.4 (date: 9.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- replaced `rcc env plan` with new `rcc holotree plan`, which now shows + installation plans from holotree spaces +- now all env commands are removed, so also toplevel "env" command is gone +- added naive helper script, deadcode.py, to find dead code +- cleaned up some dead code branches + ## v11.0.3 (date: 8.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/htfs/commands.go b/htfs/commands.go index 33091e90..7639a8b8 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -128,6 +128,11 @@ func FindEnvironment(fragment string) []string { return result } +func InstallationPlan(hash string) (string, bool) { + finalplan := filepath.Join(common.HolotreeLocation(), hash, "rcc_plan.log") + return finalplan, pathlib.IsFile(finalplan) +} + func RemoveHolotreeSpace(label string) (err error) { defer fail.Around(&err) diff --git a/operations/assistant.go b/operations/assistant.go index cc956b9d..f3c1ad76 100644 --- a/operations/assistant.go +++ b/operations/assistant.go @@ -209,19 +209,6 @@ func IoAsString(source io.Reader) string { return string(body) } -func AssistantTreeCommand(client cloud.Client, account *account, workspace string) (*WorkspaceTreeData, error) { - response, err := WorkspaceTreeCommandRequest(client, account, workspace) - if err != nil { - return nil, err - } - treedata := new(WorkspaceTreeData) - err = json.Unmarshal(response.Body, &treedata) - if err != nil { - return nil, err - } - return treedata, nil -} - func ListAssistantsCommand(client cloud.Client, account *account, workspaceId string) ([]Token, error) { credentials, err := summonAssistantToken(client, account, workspaceId) if err != nil { diff --git a/operations/credentials.go b/operations/credentials.go index 765edbc2..8c0c94bf 100644 --- a/operations/credentials.go +++ b/operations/credentials.go @@ -1,7 +1,6 @@ package operations import ( - "encoding/json" "fmt" "regexp" "sort" @@ -194,11 +193,6 @@ func ListAccounts(json bool) { } } -func EncodeCredentials(target *json.Encoder, force bool) error { - VerifyAccounts(force) - return target.Encode(smudgeSecrets(findAccounts())) -} - func loadAccount(label string) *account { prefix := accountsPrefix + label var details Token diff --git a/operations/workspaces.go b/operations/workspaces.go index 5efb45c2..d0c6c36e 100644 --- a/operations/workspaces.go +++ b/operations/workspaces.go @@ -102,35 +102,6 @@ func WorkspaceTreeCommand(client cloud.Client, account *account, workspace strin return token, nil } -func RobotDigestCommand(client cloud.Client, account *account, workspaceId, robotId string) (string, error) { - treedata, err := AssistantTreeCommand(client, account, workspaceId) - if err != nil { - return "", err - } - if treedata.Robots == nil { - return "", fmt.Errorf("Cannot find any valid robots from workspace %v!", workspaceId) - } - var selected *RobotData = nil - for _, robot := range treedata.Robots { - if robot.Id == robotId { - selected = robot - break - } - } - if selected == nil { - return "", fmt.Errorf("Cannot find robot %v from workspace %v!", robotId, workspaceId) - } - found, ok := selected.Package["sha256"] - if !ok { - return "", fmt.Errorf("Robot %v from workspace %v has no digest available!", robotId, workspaceId) - } - digest, ok := found.(string) - if !ok { - return "", fmt.Errorf("Robot %v from workspace %v has no string digest available, only %#v!", robotId, workspaceId, found) - } - return digest, nil -} - func NewRobotCommand(client cloud.Client, account *account, workspace, robotName string) (Token, error) { credentials, err := summonEditRobotToken(client, account, workspace) if err != nil { diff --git a/scripts/deadcode.py b/scripts/deadcode.py new file mode 100755 index 00000000..ca60d1a5 --- /dev/null +++ b/scripts/deadcode.py @@ -0,0 +1,50 @@ +#!/bin/env python3 + +import os +import pathlib +import re +import sys +from collections import defaultdict + +FUNC_PATTERN = re.compile(r'^\s*func\s+(\w+)') + +def read_file(filename): + with open(filename) as source: + for index, line in enumerate(source): + yield index+1, line + +def find_files(where, pattern): + return tuple(sorted(x.relative_to(where) for x in pathlib.Path(where).rglob(pattern))) + +def find_pattern(pattern, fileset): + for filename in fileset: + for number, line in read_file(filename): + for item in pattern.finditer(line): + yield f'{filename}:{number}', item.group(1) + +def process(limit): + functions = defaultdict(set) + files = find_files(os.getcwd(), '*.go') + for filename, function in find_pattern(FUNC_PATTERN, files): + functions[function].add(filename) + keys = '|'.join(sorted(functions.keys())) + pattern = re.compile(f'({keys})') + counters = defaultdict(int) + linerefs = defaultdict(set) + width = 0 + for fileref, value in find_pattern(pattern, files): + counters[value] += 1 + linerefs[value].add(fileref) + width = max(width, len(fileref)) + for key, value in sorted(counters.items()): + if key.startswith('Test'): + continue + definitions = len(functions[key]) - 1 + if value != limit + definitions: + continue + for link in sorted(linerefs[key]): + fill = ' ' * (width - len(link)) + print(f'{link}{fill} {key}') + +if __name__ == '__main__': + process(int(sys.argv[1]) if len(sys.argv) > 1 else 1) diff --git a/wizard/common.go b/wizard/common.go index 9be8505d..ad1a851a 100644 --- a/wizard/common.go +++ b/wizard/common.go @@ -1,9 +1,6 @@ package wizard import ( - "errors" - "strings" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pretty" ) @@ -22,17 +19,3 @@ func firstOf(arguments []string, missing string) string { } return missing } - -func ifThenElse(condition bool, truthy, falsy string) string { - if condition { - return truthy - } - return falsy -} - -func hasLength(value string) error { - if len(strings.TrimSpace(value)) > 2 { - return nil - } - return errors.New("Value too short!") -} From a8327d35cda30154a11b2ce3fc3586b3cdf8ea41 Mon Sep 17 00:00:00 2001 From: Kari Harju Date: Thu, 9 Sep 2021 14:10:16 +0300 Subject: [PATCH 185/516] Fixed link in environment-cache.md --- docs/environment-caching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment-caching.md b/docs/environment-caching.md index 4fd28b3b..d1040681 100644 --- a/docs/environment-caching.md +++ b/docs/environment-caching.md @@ -12,7 +12,7 @@ Instead, how about just `rcc`? RCC creates virtual execution environments that only show up as files and folders on the user machine, basically containing the complexity described above. The target is that neither setting up nor using these environments must either depend on or change anything to the user's environment. -The [hotel analogy below](/rcc/holotree#a-better-analogy-accommodations) describes the problem and evolution of our solutions. +The [hotel analogy below](/docs/environment-caching.md#a-better-analogy-accommodations) describes the problem and evolution of our solutions. ## The second evolution of environment management in RCC From b87ed5f34a0de84da1f3352f058f6d025f980572 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Fri, 10 Sep 2021 12:30:40 +0300 Subject: [PATCH 186/516] RCC-184: removal of base/live environments (v11.0.5) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - removing conda environment build related code - internal clone command was removed - side note: there is trail of FIXME comments in code for future work --- cmd/clone.go | 38 ------ common/variables.go | 2 - common/version.go | 2 +- conda/robocorp.go | 6 +- conda/workflows.go | 254 ++---------------------------------- docs/changelog.md | 7 + htfs/commands.go | 1 - robot_tests/fullrun.robot | 2 - robot_tests/holotree.robot | 2 - robot_tests/resources.robot | 2 - 10 files changed, 23 insertions(+), 293 deletions(-) delete mode 100644 cmd/clone.go diff --git a/cmd/clone.go b/cmd/clone.go deleted file mode 100644 index 2e759b2d..00000000 --- a/cmd/clone.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "github.com/robocorp/rcc/common" - "github.com/robocorp/rcc/conda" - "github.com/robocorp/rcc/pathlib" - "github.com/robocorp/rcc/pretty" - - "github.com/spf13/cobra" -) - -var cloneCmd = &cobra.Command{ - Use: "clone", - Short: "Debug tool for cloning folders.", - Long: `Internal debug tool for checking cloning speed in various disk drives.`, - Run: func(cmd *cobra.Command, args []string) { - source := cmd.LocalFlags().Lookup("source").Value.String() - target := cmd.LocalFlags().Lookup("target").Value.String() - defer common.Stopwatch("rcc internal clone lasted").Report() - success, err := conda.CloneFromTo(source, target, pathlib.CopyFile) - if err != nil { - pretty.Exit(2, "Error: Cloning failed, reason: %v!", err) - } - if !success { - pretty.Exit(1, "Error: Cloning failed.") - } - pretty.Exit(0, "Was successful: %v", success) - }, -} - -func init() { - internalCmd.AddCommand(cloneCmd) - - cloneCmd.Flags().StringP("source", "s", "", "Source directory to clone.") - cloneCmd.Flags().StringP("target", "t", "", "Source directory to clone.") - cloneCmd.MarkFlagRequired("source") - cloneCmd.MarkFlagRequired("target") -} diff --git a/common/variables.go b/common/variables.go index f648539a..f509ec96 100644 --- a/common/variables.go +++ b/common/variables.go @@ -23,7 +23,6 @@ var ( NoCache bool NoOutputCapture bool Liveonly bool - Stageonly bool StageFolder string ControllerType string HolotreeSpace string @@ -132,7 +131,6 @@ func UnifyVerbosityFlags() { func UnifyStageHandling() { if len(StageFolder) > 0 { Liveonly = true - Stageonly = true } } diff --git a/common/version.go b/common/version.go index 55b50d57..4427975d 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.4` + Version = `v11.0.5` ) diff --git a/conda/robocorp.go b/conda/robocorp.go index 83ebc290..a9261a54 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -271,10 +271,8 @@ func TemplateFrom(hash string) string { } func LiveFrom(hash string) string { - if common.Stageonly { - return common.StageFolder - } - return common.ExpandPath(filepath.Join(common.LiveLocation(), hash)) + // FIXME: remove when base/live is erased + return common.StageFolder } func TemplateList() []string { diff --git a/conda/workflows.go b/conda/workflows.go index 7cb43217..ea1c2a50 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -9,7 +9,6 @@ import ( "math/rand" "os" "path/filepath" - "runtime" "strings" "time" @@ -31,62 +30,15 @@ func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } -func metaLoad(location string) (string, error) { - raw, err := ioutil.ReadFile(metafile(location)) - if err != nil { - return "", err - } - result := string(raw) - common.Trace("[Load] Metafile %q content: %s", location, result) - return result, nil -} - func metaSave(location, data string) error { - if common.Stageonly { - return nil - } - common.Trace("[Save] Metafile %q content: %s", location, data) - return ioutil.WriteFile(metafile(location), []byte(data), 0644) -} - -func touchMetafile(location string) { - pathlib.TouchWhen(metafile(location), time.Now()) + // FIXME: remove when base/live is erased + return nil } func LastUsed(location string) (time.Time, error) { return pathlib.Modtime(metafile(location)) } -func IsPristine(folder string) bool { - digest, err := DigestFor(folder, nil) - if err != nil { - common.Trace("Calculating digest for folder %q failed, reason %v.", folder, err) - return false - } - meta, err := metaLoad(folder) - if err != nil { - common.Trace("Loading metafile for folder %q failed, reason %v.", folder, err) - return false - } - return Hexdigest(digest) == meta -} - -func reuseExistingLive(key string) (bool, error) { - if common.Stageonly { - return false, nil - } - candidate := LiveFrom(key) - if IsPristine(candidate) { - touchMetafile(candidate) - return true, nil - } - err := renameRemove(candidate) - if err != nil { - return false, err - } - return false, nil -} - func livePrepare(liveFolder string, command ...string) (*shell.Task, error) { searchPath := FindPath(liveFolder) commandName := command[0] @@ -150,7 +102,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall if !MustMicromamba() { return false, fmt.Errorf("Could not get micromamba installed.") } - targetFolder := LiveFrom(key) + targetFolder := common.StageFolder common.Debug("=== new live --- pre cleanup phase ===") common.Timeline("pre cleanup phase.") err := renameRemove(targetFolder) @@ -176,7 +128,7 @@ func newLive(yaml, condaYaml, requirementsText, key string, force, freshInstall } func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, freshInstall bool, postInstall []string) (bool, bool) { - targetFolder := LiveFrom(key) + targetFolder := common.StageFolder planfile := fmt.Sprintf("%s.plan", targetFolder) planWriter, err := os.OpenFile(planfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { @@ -371,12 +323,10 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { misses := xviper.GetInt("stats.env.miss") failures := xviper.GetInt("stats.env.failures") merges := xviper.GetInt("stats.env.merges") - templates := len(TemplateList()) - freshInstall := templates == 0 + freshInstall := true defer func() { - templates = len(TemplateList()) - common.Log("#### Progress: 6/6 [Done.] [Cache statistics: %d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", templates, requests, merges, hits, dirty, misses, failures, common.Version) + common.Log("#### Progress: 6/6 [Done.] [Cache statistics: %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", requests, merges, hits, dirty, misses, failures, common.Version) common.Timeline("6/6 Done.") }() common.Log("#### Progress: 0/6 [try use existing live same environment?] %v", xviper.TrackingIdentity()) @@ -403,48 +353,12 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { common.Log("#### Progress: 1/6 [environment key is: %s (deprecated)]", key) common.Timeline("1/6 key %s (deprecated).", key) - common.EnvironmentHash = key - - liveFolder := LiveFrom(key) - after := make(map[string]string) - afterHash, afterErr := DigestFor(liveFolder, after) - reusable, err := reuseExistingLive(key) - if err != nil { - return "", err - } - if !force && reusable { - hits += 1 - xviper.Set("stats.env.hit", hits) - return liveFolder, nil - } - if force || common.Stageonly { - if force { - common.Log("#### Progress: 2/6 [skipped -- forced]") - } else { - common.Log("#### Progress: 2/6 [skipped -- stage only]") - common.Timeline("2/6 stage only.") - } + liveFolder := common.StageFolder + if force { + common.Log("#### Progress: 2/6 [skipped -- forced]") } else { - templateFolder := TemplateFrom(key) - if IsPristine(templateFolder) { - common.Trace("Template is pristine: %q", templateFolder) - before := make(map[string]string) - beforeHash, beforeErr := DigestFor(templateFolder, before) - DiagnoseDirty(templateFolder, liveFolder, beforeHash, afterHash, beforeErr, afterErr, before, after, false) - } else if pathlib.IsDir(templateFolder) { - common.Log("WARNING! Template is NOT pristine: %q", templateFolder) - } - common.Log("#### Progress: 2/6 [try clone existing same template to live, key: %v]", key) - common.Timeline("2/6 base to live.") - success, err := CloneFromTo(templateFolder, liveFolder, pathlib.CopyFile) - if err != nil { - return "", err - } - if success { - dirty += 1 - xviper.Set("stats.env.dirty", dirty) - return liveFolder, nil - } + common.Log("#### Progress: 2/6 [skipped -- stage only]") + common.Timeline("2/6 stage only.") } common.Log("#### Progress: 3/6 [try create new environment from scratch]") common.Timeline("3/6 env from scratch.") @@ -455,17 +369,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { if success { misses += 1 xviper.Set("stats.env.miss", misses) - if common.Liveonly { - common.Log("#### Progress: 5/6 [skipped -- live only]") - common.Timeline("5/6 live only.") - } else { - common.Log("#### Progress: 5/6 [backup new environment as template]") - common.Timeline("5/6 backup to base.") - _, err = CloneFromTo(liveFolder, TemplateFrom(key), pathlib.CopyFile) - if err != nil { - return "", err - } - } + common.Log("#### Progress: 5/6 [skipped -- live only]") + common.Timeline("5/6 live only.") return liveFolder, nil } @@ -510,136 +415,3 @@ func renameRemove(location string) error { common.Trace("Metafile %q was not file.", meta) return nil } - -func CloneFromTo(source, target string, copier pathlib.Copier) (bool, error) { - common.Trace("Start CloneFromTo: %q -> %q", source, target) - defer common.Trace("Done CloneFromTo: %q -> %q", source, target) - err := renameRemove(target) - if err != nil { - return false, err - } - os.MkdirAll(target, 0755) - - if !IsPristine(source) { - if common.Liveonly { - common.Debug("Clone source %q is dirty, but wont remove since --liveonly flag.", source) - } else { - err = renameRemove(source) - if err != nil { - return false, fmt.Errorf("Source %q is not pristine! And could not remove: %v", source, err) - } - common.Trace("Source %q dirty. And it was removed.", source) - } - return false, nil - } - expected, err := metaLoad(source) - if err != nil { - common.Trace("Metafile load %q failed with %v.", source, err) - return false, nil - } - success := cloneFolder(source, target, 100, copier) - if !success { - err = renameRemove(target) - if err != nil { - return false, fmt.Errorf("Cloning %q to %q failed! And cleanup failed: %v", source, target, err) - } - common.Trace("Cloing %q -> %q failed.", source, target) - return false, nil - } - digest, err := DigestFor(target, nil) - if err != nil || Hexdigest(digest) != expected { - common.Trace("Digest %q failed, %s vs %s or error %v.", target, Hexdigest(digest), expected, err) - origin := make(map[string]string) - originHash, originErr := DigestFor(source, origin) - created := make(map[string]string) - createdHash, createdErr := DigestFor(target, created) - DiagnoseDirty(source, target, originHash, createdHash, originErr, createdErr, origin, created, false) - err = renameRemove(target) - if err != nil { - return false, fmt.Errorf("Target %q does not match source %q! And cleanup failed: %v!", target, source, err) - } - return false, nil - } - metaSave(target, expected) - touchMetafile(source) - common.Trace("Success CloneFromTo: %q -> %q", source, target) - return true, nil -} - -func cloneFolder(source, target string, workers int, copier pathlib.Copier) bool { - queue := make(chan copyRequest) - done := make(chan bool) - - for x := 0; x < workers; x++ { - go copyWorker(queue, done, copier) - } - runtime.Gosched() - - success := copyFolder(source, target, queue) - close(queue) - - for x := 0; x < workers; x++ { - <-done - } - - return success -} - -func copyFolder(source, target string, queue chan copyRequest) bool { - os.Mkdir(target, 0755) - - handle, err := os.Open(source) - if err != nil { - common.Error("OPEN", err) - return false - } - entries, err := handle.Readdir(-1) - handle.Close() - if err != nil { - common.Error("DIR", err) - return false - } - - success := true - expect := 0 - for _, entry := range entries { - if entry.Name() == "__pycache__" { - continue - } - newSource := filepath.Join(source, entry.Name()) - newTarget := filepath.Join(target, entry.Name()) - if entry.IsDir() { - copyFolder(newSource, newTarget, queue) - } else { - queue <- copyRequest{newSource, newTarget} - expect += 1 - } - } - - return success -} - -type copyRequest struct { - source, target string -} - -func copyWorker(tasks chan copyRequest, done chan bool, copier pathlib.Copier) { - for { - task, ok := <-tasks - if !ok { - break - } - link, err := os.Readlink(task.source) - if err != nil { - copier(task.source, task.target, false) - continue - } - err = os.Symlink(link, task.target) - if err != nil { - common.Error("LINK", err) - continue - } - } - - done <- true -} diff --git a/docs/changelog.md b/docs/changelog.md index bbe8d358..a51a3ccc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.0.5 (date: 10.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- removing conda environment build related code +- internal clone command was removed +- side note: there is trail of FIXME comments in code for future work + ## v11.0.4 (date: 9.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/htfs/commands.go b/htfs/commands.go index 7639a8b8..70ae5106 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -89,7 +89,6 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e // following must be setup here common.StageFolder = tree.Stage() - common.Stageonly = true common.Liveonly = true common.Debug("Holotree stage is %q.", tree.Stage()) diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 58cfda0b..468b7777 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -102,8 +102,6 @@ Goal: Run task in place in debug mode and with timeline. Must Have Status Must Have OK. Must Exist tmp/fluffy/output/environment_*_freeze.yaml - Must Exist %{ROBOCORP_HOME}/base/ - Must Exist %{ROBOCORP_HOME}/live/ Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 3a2ab82c..8c402574 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -126,8 +126,6 @@ Goal: Do quick cleanup on environments Must Exist %{ROBOCORP_HOME}/hololib/ Must Exist %{ROBOCORP_HOME}/pkgs/ Wont Exist %{ROBOCORP_HOME}/holotree/ - Wont Exist %{ROBOCORP_HOME}/base/ - Wont Exist %{ROBOCORP_HOME}/live/ Wont Exist %{ROBOCORP_HOME}/pipcache/ Use STDERR Must Have OK diff --git a/robot_tests/resources.robot b/robot_tests/resources.robot index 9f31f6bd..d3ec4be7 100644 --- a/robot_tests/resources.robot +++ b/robot_tests/resources.robot @@ -18,8 +18,6 @@ Prepare Local Comment Verify micromamba is installed or download and install it. Step build/rcc ht vars robot_tests/conda.yaml Must Exist %{ROBOCORP_HOME}/bin/ - Must Exist %{ROBOCORP_HOME}/base/ - Must Exist %{ROBOCORP_HOME}/live/ Must Exist %{ROBOCORP_HOME}/wheels/ Must Exist %{ROBOCORP_HOME}/pipcache/ From 96b6193185b9fac2dae14ccb8bffc22b92606fc8 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Mon, 13 Sep 2021 10:28:50 +0300 Subject: [PATCH 187/516] RCC-184: removal of base/live environments (v11.0.6) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - removing options from cleanup commands, since those are base/live specific and not needed anymore (orphans, miniconda) - removed dead code resulted from above --- cmd/cleanup.go | 6 +-- common/variables.go | 10 +---- common/version.go | 2 +- conda/cleanup.go | 73 +++++++++----------------------- conda/robocorp.go | 89 --------------------------------------- conda/workflows.go | 8 ---- docs/changelog.md | 7 +++ operations/diagnostics.go | 3 +- 8 files changed, 30 insertions(+), 168 deletions(-) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index e0315600..97270a7f 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -11,8 +11,6 @@ import ( var ( allFlag bool quickFlag bool - orphanFlag bool - minicondaFlag bool micromambaFlag bool daysOption int ) @@ -26,7 +24,7 @@ After cleanup, they will not be available anymore.`, if common.DebugFlag { defer common.Stopwatch("Env cleanup lasted").Report() } - err := conda.Cleanup(daysOption, dryFlag, orphanFlag, quickFlag, allFlag, minicondaFlag, micromambaFlag) + err := conda.Cleanup(daysOption, dryFlag, quickFlag, allFlag, micromambaFlag) if err != nil { pretty.Exit(1, "Error: %v", err) } @@ -37,8 +35,6 @@ After cleanup, they will not be available anymore.`, func init() { configureCmd.AddCommand(cleanupCmd) cleanupCmd.Flags().BoolVarP(&dryFlag, "dryrun", "d", false, "Don't delete environments, just show what would happen.") - cleanupCmd.Flags().BoolVarP(&orphanFlag, "orphans", "o", false, "Cleanup orphan, unreachable enviroments.") - cleanupCmd.Flags().BoolVarP(&minicondaFlag, "miniconda", "m", false, "Remove miniconda3 installation (replaced by micromamba).") cleanupCmd.Flags().BoolVarP(µmambaFlag, "micromamba", "", false, "Remove micromamba installation.") cleanupCmd.Flags().BoolVarP(&allFlag, "all", "", false, "Cleanup all enviroments.") cleanupCmd.Flags().BoolVarP(&quickFlag, "quick", "q", false, "Cleanup most of enviroments, but leave hololib and pkgs cache intact.") diff --git a/common/variables.go b/common/variables.go index f509ec96..2e3c3a32 100644 --- a/common/variables.go +++ b/common/variables.go @@ -46,7 +46,7 @@ func RobocorpHome() string { } func RobocorpLock() string { - return fmt.Sprintf("%s.lck", LiveLocation()) + return filepath.Join(RobocorpHome(), "robocorp.lck") } func VerboseEnvironmentBuilding() bool { @@ -74,14 +74,6 @@ func BinLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "bin")) } -func LiveLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "live")) -} - -func BaseLocation() string { - return ensureDirectory(filepath.Join(RobocorpHome(), "base")) -} - func HololibLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "hololib")) } diff --git a/common/version.go b/common/version.go index 4427975d..646a5032 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.5` + Version = `v11.0.6` ) diff --git a/conda/cleanup.go b/conda/cleanup.go index 9ab515df..ef501df8 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -38,41 +38,32 @@ func doCleanup(fullpath string, dryrun bool) error { common.Log("Would be removing: %s", fullpath) return nil } - return os.RemoveAll(fullpath) + return safeRemove("path", fullpath) } -func orphanCleanup(dryrun bool) error { - orphans := OrphanList() - if len(orphans) == 0 { - return nil - } +func alwaysCleanup(dryrun bool) { + base := filepath.Join(common.RobocorpHome(), "base") + live := filepath.Join(common.RobocorpHome(), "live") + miniconda3 := filepath.Join(common.RobocorpHome(), "miniconda3") if dryrun { - common.Log("Would be removing orphans:") - for _, orphan := range orphans { - common.Log("- %v", orphan) - } - return nil - } - for _, orphan := range orphans { - safeRemove("orphan", orphan) - } - return nil + common.Log("Would be removing:") + common.Log("- %v", base) + common.Log("- %v", live) + common.Log("- %v", miniconda3) + return + } + safeRemove("legacy", base) + safeRemove("legacy", live) + safeRemove("legacy", miniconda3) } func quickCleanup(dryrun bool) error { if dryrun { - common.Log("Would be removing:") - common.Log("- %v", common.BaseLocation()) - common.Log("- %v", common.LiveLocation()) - common.Log("- %v", MinicondaLocation()) common.Log("- %v", common.PipCache()) common.Log("- %v", common.HolotreeLocation()) common.Log("- %v", RobocorpTempRoot()) return nil } - safeRemove("cache", common.BaseLocation()) - safeRemove("cache", common.LiveLocation()) - safeRemove("cache", MinicondaLocation()) safeRemove("cache", common.PipCache()) err := safeRemove("cache", common.HolotreeLocation()) if err != nil { @@ -130,7 +121,7 @@ func cleanupTemp(deadline time.Time, dryrun bool) error { return nil } -func Cleanup(daylimit int, dryrun, orphans, quick, all, miniconda, micromamba bool) error { +func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { lockfile := common.RobocorpLock() locker, err := pathlib.Locker(lockfile, 30000) if err != nil { @@ -139,6 +130,8 @@ func Cleanup(daylimit int, dryrun, orphans, quick, all, miniconda, micromamba bo } defer locker.Release() + alwaysCleanup(dryrun) + if quick { return quickCleanup(dryrun) } @@ -147,37 +140,9 @@ func Cleanup(daylimit int, dryrun, orphans, quick, all, miniconda, micromamba bo return spotlessCleanup(dryrun) } - deadline := time.Now().Add(-24 * time.Duration(daylimit) * time.Hour) + deadline := time.Now().Add(-48 * time.Duration(daylimit) * time.Hour) cleanupTemp(deadline, dryrun) - for _, template := range TemplateList() { - whenLive, err := LastUsed(LiveFrom(template)) - if err != nil { - return err - } - if !all && whenLive.After(deadline) { - continue - } - whenBase, err := LastUsed(TemplateFrom(template)) - if err != nil { - return err - } - if !all && whenBase.After(deadline) { - continue - } - if dryrun { - common.Log("Would be removing %v.", template) - continue - } - // FIXME: remove this when base/live removal is done - RemoveEnvironment(template) - common.Debug("Removed environment %v.", template) - } - if orphans { - err = orphanCleanup(dryrun) - } - if miniconda && err == nil { - err = doCleanup(MinicondaLocation(), dryrun) - } + if micromamba && err == nil { err = doCleanup(MambaPackages(), dryrun) } diff --git a/conda/robocorp.go b/conda/robocorp.go index a9261a54..2d8b9460 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -74,76 +74,6 @@ func DigestFor(folder string, collect map[string]string) ([]byte, error) { return result, nil } -func hashedEntity(name string) bool { - return hashPattern.MatchString(name) -} - -func hasDatadir(basedir, metafile string) bool { - if filepath.Ext(metafile) != ".meta" { - return false - } - fullpath := filepath.Join(basedir, metafile) - stat, err := os.Stat(fullpath[:len(fullpath)-5]) - return err == nil && stat.IsDir() -} - -func hasMetafile(basedir, subdir string) bool { - folder := filepath.Join(basedir, subdir) - _, err := os.Stat(metafile(folder)) - return err == nil -} - -func dirnamesFrom(location string) []string { - result := make([]string, 0, 20) - handle, err := os.Open(common.ExpandPath(location)) - if err != nil { - common.Error("Warning", err) - return result - } - defer handle.Close() - children, err := handle.Readdir(-1) - if err != nil { - common.Error("Warning", err) - return result - } - - for _, child := range children { - if child.IsDir() && hasMetafile(location, child.Name()) { - result = append(result, child.Name()) - } - } - - return result -} - -func orphansFrom(location string) []string { - result := make([]string, 0, 20) - handle, err := os.Open(common.ExpandPath(location)) - if err != nil { - common.Error("Warning", err) - return result - } - defer handle.Close() - children, err := handle.Readdir(-1) - if err != nil { - common.Error("Warning", err) - return result - } - - for _, child := range children { - hashed := hashedEntity(child.Name()) - if hashed && child.IsDir() && hasMetafile(location, child.Name()) { - continue - } - if hashed && !child.IsDir() && hasDatadir(location, child.Name()) { - continue - } - result = append(result, filepath.Join(location, child.Name())) - } - - return result -} - func FindPath(environment string) pathlib.PathParts { target := pathlib.TargetPath() target = target.Remove(ignoredPaths) @@ -248,11 +178,6 @@ func RobocorpTemp() string { return fullpath } -func MinicondaLocation() string { - // Legacy function, but must remain until cleanup is done - return filepath.Join(common.RobocorpHome(), "miniconda3") -} - func LocalChannel() (string, bool) { basefolder := filepath.Join(common.RobocorpHome(), "channel") fullpath := filepath.Join(basefolder, "channeldata.json") @@ -266,25 +191,11 @@ func LocalChannel() (string, bool) { return "", false } -func TemplateFrom(hash string) string { - return filepath.Join(common.BaseLocation(), hash) -} - func LiveFrom(hash string) string { // FIXME: remove when base/live is erased return common.StageFolder } -func TemplateList() []string { - return dirnamesFrom(common.BaseLocation()) -} - -func OrphanList() []string { - result := orphansFrom(common.BaseLocation()) - result = append(result, orphansFrom(common.LiveLocation())...) - return result -} - func InstallationPlan(hash string) (string, bool) { finalplan := filepath.Join(LiveFrom(hash), "rcc_plan.log") return finalplan, pathlib.IsFile(finalplan) diff --git a/conda/workflows.go b/conda/workflows.go index ea1c2a50..ca5cb6fd 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -379,14 +379,6 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { return "", errors.New("Could not create environment.") } -func RemoveEnvironment(label string) error { - err := renameRemove(LiveFrom(label)) - if err != nil { - return err - } - return renameRemove(TemplateFrom(label)) -} - func renameRemove(location string) error { if !pathlib.IsDir(location) { common.Trace("Location %q is not directory, not removed.", location) diff --git a/docs/changelog.md b/docs/changelog.md index a51a3ccc..c381f4ed 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.0.6 (date: 13.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- removing options from cleanup commands, since those are base/live specific + and not needed anymore (orphans, miniconda) +- removed dead code resulted from above + ## v11.0.5 (date: 10.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/operations/diagnostics.go b/operations/diagnostics.go index ab78f7d4..59c36ef3 100644 --- a/operations/diagnostics.go +++ b/operations/diagnostics.go @@ -93,8 +93,7 @@ func rccStatusLine() string { misses := xviper.GetInt("stats.env.miss") failures := xviper.GetInt("stats.env.failures") merges := xviper.GetInt("stats.env.merges") - templates := len(conda.TemplateList()) - return fmt.Sprintf("%d environments, %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s", templates, requests, merges, hits, dirty, misses, failures, xviper.TrackingIdentity()) + return fmt.Sprintf("%d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s", requests, merges, hits, dirty, misses, failures, xviper.TrackingIdentity()) } func longPathSupportCheck() *common.DiagnosticCheck { From c8564386deede8152c4dfbb57b032f4536a287a6 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Tue, 14 Sep 2021 11:19:26 +0300 Subject: [PATCH 188/516] RCC-184: removal of base/live environments (v11.0.7) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - changed progress indication to match holotree flow - made log and telemetry waiting visible in timeline --- cloud/metrics.go | 2 ++ cmd/rcc/main.go | 4 ++- common/logger.go | 2 ++ common/version.go | 2 +- conda/workflows.go | 43 +++++++++++--------------------- docs/changelog.md | 6 +++++ htfs/commands.go | 15 ++++++++--- robot_tests/export_holozip.robot | 4 +-- robot_tests/fullrun.robot | 21 ++++++++-------- robot_tests/holotree.robot | 3 --- 10 files changed, 53 insertions(+), 49 deletions(-) diff --git a/cloud/metrics.go b/cloud/metrics.go index a304a213..09f9e9e6 100644 --- a/cloud/metrics.go +++ b/cloud/metrics.go @@ -54,6 +54,8 @@ func BackgroundMetric(kind, name, value string) { } func WaitTelemetry() { + defer common.Timeline("wait telemetry done") + common.Debug("wait telemetry to complete") telemetryBarrier.Wait() common.Debug("telemetry sending completed") diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 45fbb6d7..048a4cee 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -88,13 +88,15 @@ func markTempForRecycling() { func main() { common.Timeline("Start.") + defer common.EndOfTimeline() + defer ExitProtection() - defer common.EndOfTimeline() go startTempRecycling() defer markTempForRecycling() defer os.Stderr.Sync() defer os.Stdout.Sync() cmd.Execute() + common.Timeline("Command execution done.") TimezoneMetric() } diff --git a/common/logger.go b/common/logger.go index 5e3f5039..99fa6969 100644 --- a/common/logger.go +++ b/common/logger.go @@ -92,5 +92,7 @@ func Stdout(format string, details ...interface{}) { } func WaitLogs() { + defer Timeline("wait logs done") + logbarrier.Wait() } diff --git a/common/version.go b/common/version.go index 646a5032..9cd0d0fb 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.6` + Version = `v11.0.7` ) diff --git a/conda/workflows.go b/conda/workflows.go index ca5cb6fd..36357b2c 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -152,12 +152,12 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } + Progress(5, "Running micromamba phase.") mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") observer := make(InstallObserver) common.Debug("=== new live --- micromamba create phase ===") - common.Timeline("Micromamba start.") fmt.Fprintf(planWriter, "\n--- micromamba plan @%ss ---\n\n", stopwatch) tee := io.MultiWriter(observer, planWriter) code, err := shell.New(CondaEnvironment(), ".", mambaCommand.CLI()...).Tracked(tee, false) @@ -175,11 +175,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - common.Log("#### Progress: 4/6 [pip install phase skipped -- no pip dependencies]") - common.Timeline("4/6 no pip.") + Progress(6, "Skipping pip install phase -- no pip dependencies.") } else { - common.Log("#### Progress: 4/6 [pip install phase]") - common.Timeline("4/6 pip install start.") + Progress(6, "Running pip install phase.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander("pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -198,7 +196,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - common.Timeline("post install.") + Progress(7, "Post install scripts phase started.") common.Debug("=== new live --- post install phase ===") for _, script := range postInstall { scriptCommand, err := shlex.Split(script) @@ -215,7 +213,10 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } } + } else { + Progress(7, "Post install scripts phase skipped -- no scripts.") } + Progress(8, "Activate environment started phase.") common.Debug("=== new live --- activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) err = Activate(planWriter, targetFolder) @@ -232,6 +233,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planWriter.Sync() planWriter.Close() + Progress(9, "Update installation plan.") finalplan, _ := InstallationPlan(key) os.Rename(planfile, finalplan) common.Log("%sInstallation plan is: %v%s", pretty.Yellow, finalplan, pretty.Reset) @@ -303,7 +305,13 @@ func CalculateComboHash(configurations ...string) (string, error) { return key, nil } -func NewEnvironment(force bool, configurations ...string) (string, error) { +func Progress(step int, form string, details ...interface{}) { + message := fmt.Sprintf(form, details...) + common.Log("#### Progress: %d/12 %s %s", step, common.Version, message) + common.Timeline("%d/12 %s", step, message) +} + +func LegacyEnvironment(force bool, configurations ...string) (string, error) { common.Timeline("New environment.") cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) @@ -318,20 +326,11 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { defer locker.Release() requests := xviper.GetInt("stats.env.request") + 1 - hits := xviper.GetInt("stats.env.hit") - dirty := xviper.GetInt("stats.env.dirty") misses := xviper.GetInt("stats.env.miss") failures := xviper.GetInt("stats.env.failures") merges := xviper.GetInt("stats.env.merges") freshInstall := true - defer func() { - common.Log("#### Progress: 6/6 [Done.] [Cache statistics: %d requests, %d merges, %d hits, %d dirty, %d misses, %d failures | %s]", requests, merges, hits, dirty, misses, failures, common.Version) - common.Timeline("6/6 Done.") - }() - common.Log("#### Progress: 0/6 [try use existing live same environment?] %v", xviper.TrackingIdentity()) - common.Timeline("0/6 Existing.") - xviper.Set("stats.env.request", requests) if len(configurations) > 1 { @@ -350,18 +349,8 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { } defer os.Remove(condaYaml) defer os.Remove(requirementsText) - common.Log("#### Progress: 1/6 [environment key is: %s (deprecated)]", key) - common.Timeline("1/6 key %s (deprecated).", key) liveFolder := common.StageFolder - if force { - common.Log("#### Progress: 2/6 [skipped -- forced]") - } else { - common.Log("#### Progress: 2/6 [skipped -- stage only]") - common.Timeline("2/6 stage only.") - } - common.Log("#### Progress: 3/6 [try create new environment from scratch]") - common.Timeline("3/6 env from scratch.") success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) if err != nil { return "", err @@ -369,8 +358,6 @@ func NewEnvironment(force bool, configurations ...string) (string, error) { if success { misses += 1 xviper.Set("stats.env.miss", misses) - common.Log("#### Progress: 5/6 [skipped -- live only]") - common.Timeline("5/6 live only.") return liveFolder, nil } diff --git a/docs/changelog.md b/docs/changelog.md index c381f4ed..c228371d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v11.0.7 (date: 14.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- changed progress indication to match holotree flow +- made log and telemetry waiting visible in timeline + ## v11.0.6 (date: 13.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/htfs/commands.go b/htfs/commands.go index 70ae5106..2af93fec 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -14,6 +14,7 @@ import ( "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/pathlib" "github.com/robocorp/rcc/robot" + "github.com/robocorp/rcc/xviper" ) func Platform() string { @@ -23,6 +24,9 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) + defer conda.Progress(12, "Fresh holotree done.") + conda.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) + callback := pathlib.LockWaitMessage("Serialized environment creation") locker, err := pathlib.Locker(common.HolotreeLock(), 30000) callback() @@ -31,15 +35,15 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er haszip := len(holozip) > 0 - common.Timeline("new holotree environment") _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) + common.EnvironmentHash = BlueprintHash(holotreeBlueprint) + conda.Progress(2, "Holotree blueprint is %q.", common.EnvironmentHash) anywork.Scale(100) tree, err := New() fail.On(err != nil, "%s", err) - common.Timeline("holotree library created") if !haszip && !tree.HasBlueprint(holotreeBlueprint) && common.Liveonly { tree = Virtual() @@ -56,9 +60,9 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er library = tree } + conda.Progress(11, "Restore space from library.") path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) - common.EnvironmentHash = BlueprintHash(holotreeBlueprint) return path, nil } @@ -96,19 +100,22 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e common.Debug("Has blueprint environment: %v", exists) if force || !exists { + conda.Progress(3, "Cleanup holotree stage for fresh install.") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) + conda.Progress(4, "Build environment into holotree stage.") identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = ioutil.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) - label, err := conda.NewEnvironment(force, identityfile) + label, err := conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) common.Debug("Label: %q", label) + conda.Progress(10, "Record holotree stage to hololib.") err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index 5b02b484..c81130d9 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -32,8 +32,8 @@ Goal: Create environment for standalone robot Must Have 4e67cd8d4_fcb4b859 Use STDERR Must Have Downloading micromamba - Must Have Progress: 4/6 - Must Have Progress: 6/6 + Must Have Progress: 1/12 + Must Have Progress: 12/12 Goal: Must have author space visible Step build/rcc ht ls diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index 468b7777..f146f861 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -77,9 +77,10 @@ Goal: Run task in place in debug mode and with timeline. Step build/rcc task run --task "Run Example task" --controller citests -r tmp/fluffy/robot.yaml --debug --timeline Must Have 1 task, 1 passed, 0 failed Use STDERR - Must Have Progress: 0/6 - Must Have Progress: 1/6 - Must Have Progress: 6/6 + Must Have Progress: 1/12 + Must Have Progress: 2/12 + Must Have Progress: 3/12 + Must Have Progress: 12/12 Must Have rpaframework Must Have PID # Must Have [N] @@ -110,13 +111,13 @@ Goal: Run task in clean temporary directory. Must Have 1 task, 1 passed, 0 failed Use STDERR Must Have rpaframework - Wont Have Progress: 0/6 - Wont Have Progress: 1/6 - Wont Have Progress: 2/6 - Wont Have Progress: 3/6 - Wont Have Progress: 4/6 - Wont Have Progress: 5/6 - Wont Have Progress: 6/6 + Must Have Progress: 1/12 + Wont Have Progress: 3/12 + Wont Have Progress: 5/12 + Wont Have Progress: 7/12 + Wont Have Progress: 9/12 + Must Have Progress: 11/12 + Must Have Progress: 12/12 Must Have OK. Goal: Merge two different conda.yaml files with conflict fails diff --git a/robot_tests/holotree.robot b/robot_tests/holotree.robot index 8c402574..e972cf10 100644 --- a/robot_tests/holotree.robot +++ b/robot_tests/holotree.robot @@ -89,7 +89,6 @@ Goal: See variables from specific environment with robot.yaml knowledge in JSON Must Be Json Response Use STDERR Wont Have (virtual) - Wont Have live only Goal: Liveonly works and uses virtual holotree Step build/rcc holotree vars --liveonly --space jam --controller citests robot_tests/certificates.yaml --config tmp/alternative.yaml --timeline @@ -118,7 +117,6 @@ Goal: Liveonly works and uses virtual holotree Wont Have RC_WORKSPACE_ID= Use STDERR Must Have (virtual) - Must Have live only Goal: Do quick cleanup on environments Step build/rcc config cleanup --controller citests --quick @@ -135,4 +133,3 @@ Goal: Liveonly works and uses virtual holotree and can give output in JSON form Must Be Json Response Use STDERR Must Have (virtual) - Must Have live only From 7133437bfc99becfced1afb9af0fd6b0dcf9de9a Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Wed, 15 Sep 2021 13:38:55 +0300 Subject: [PATCH 189/516] RCC-184: removal of base/live environments (v11.0.8) - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) - showing correct `rcc_plan.log` and `identity.yaml` files on log - reorganizing some common code away from conda module - rpaframework upgrade to version 11.1.3 in templates --- cmd/dirhash.go | 2 +- cmd/rcc/main.go | 2 +- common/algorithms.go | 13 ++++++ common/logger.go | 6 +++ common/variables.go | 16 +++++++ common/version.go | 2 +- conda/activate.go | 12 +---- conda/cleanup.go | 12 ++--- conda/robocorp.go | 20 +-------- conda/workflows.go | 76 +++++++------------------------- docs/changelog.md | 7 +++ htfs/commands.go | 17 ++++--- htfs/library.go | 9 ++++ robot/robot.go | 14 ------ robot_tests/export_holozip.robot | 12 ++--- robot_tests/fullrun.robot | 2 +- templates/extended/conda.yaml | 2 +- templates/python/conda.yaml | 2 +- templates/standard/conda.yaml | 2 +- 19 files changed, 97 insertions(+), 131 deletions(-) diff --git a/cmd/dirhash.go b/cmd/dirhash.go index 07b5e009..26208327 100644 --- a/cmd/dirhash.go +++ b/cmd/dirhash.go @@ -47,7 +47,7 @@ var dirhashCmd = &cobra.Command{ } collector = conda.MakeRelativeMap(fullpath, collector) diffMaps = append(diffMaps, collector) - result := conda.Hexdigest(digest) + result := common.Hexdigest(digest) common.Log("+ %v %v", result, directory) if showIntermediateDirhashes { relative := make(map[string]string) diff --git a/cmd/rcc/main.go b/cmd/rcc/main.go index 048a4cee..66cc68c3 100644 --- a/cmd/rcc/main.go +++ b/cmd/rcc/main.go @@ -60,7 +60,7 @@ func ExitProtection() { } func startTempRecycling() { - pattern := filepath.Join(conda.RobocorpTempRoot(), "*", "recycle.now") + pattern := filepath.Join(common.RobocorpTempRoot(), "*", "recycle.now") found, err := filepath.Glob(pattern) if err != nil { common.Debug("Recycling failed, reason: %v", err) diff --git a/common/algorithms.go b/common/algorithms.go index 3a898c0d..0ed5bed2 100644 --- a/common/algorithms.go +++ b/common/algorithms.go @@ -1,6 +1,8 @@ package common import ( + "crypto/sha256" + "fmt" "math" ) @@ -24,3 +26,14 @@ func Entropy(input []byte) float64 { } return entropy / 8.0 } + +func Hexdigest(raw []byte) string { + return fmt.Sprintf("%02x", raw) +} + +func ShortDigest(content string) string { + digester := sha256.New() + digester.Write([]byte(content)) + result := Hexdigest(digester.Sum(nil)) + return result[:16] +} diff --git a/common/logger.go b/common/logger.go index 99fa6969..0852a72c 100644 --- a/common/logger.go +++ b/common/logger.go @@ -96,3 +96,9 @@ func WaitLogs() { logbarrier.Wait() } + +func Progress(step int, form string, details ...interface{}) { + message := fmt.Sprintf(form, details...) + Log("#### Progress: %d/12 %s %s", step, Version, message) + Timeline("%d/12 %s", step, message) +} diff --git a/common/variables.go b/common/variables.go index 2e3c3a32..46101811 100644 --- a/common/variables.go +++ b/common/variables.go @@ -62,6 +62,14 @@ func ensureDirectory(name string) string { return name } +func BinRcc() string { + self, err := os.Executable() + if err != nil { + return os.Args[0] + } + return self +} + func EventJournal() string { return filepath.Join(RobocorpHome(), "event.log") } @@ -70,6 +78,10 @@ func TemplateLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "templates")) } +func RobocorpTempRoot() string { + return filepath.Join(RobocorpHome(), "temp") +} + func BinLocation() string { return ensureDirectory(filepath.Join(RobocorpHome(), "bin")) } @@ -110,6 +122,10 @@ func RobotCache() string { return ensureDirectory(filepath.Join(RobocorpHome(), "robots")) } +func MambaPackages() string { + return ExpandPath(filepath.Join(RobocorpHome(), "pkgs")) +} + func UnifyVerbosityFlags() { if Silent { DebugFlag = false diff --git a/common/version.go b/common/version.go index 9cd0d0fb..4cad3a07 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v11.0.7` + Version = `v11.0.8` ) diff --git a/conda/activate.go b/conda/activate.go index 1a74a535..63ed582e 100644 --- a/conda/activate.go +++ b/conda/activate.go @@ -18,14 +18,6 @@ const ( activateFile = "rcc_activate.json" ) -func BinRcc() string { - self, err := os.Executable() - if err != nil { - return os.Args[0] - } - return self -} - func capturePreformatted(incoming string) ([]string, string) { lines := strings.SplitAfter(incoming, "\n") capture := false @@ -61,7 +53,7 @@ func createScript(targetFolder string) (string, error) { return "", err } details := make(map[string]string) - details["Rcc"] = BinRcc() + details["Rcc"] = common.BinRcc() details["Robocorphome"] = common.RobocorpHome() details["Micromamba"] = BinMicromamba() details["Live"] = targetFolder @@ -106,7 +98,7 @@ func diffStringMaps(before, after map[string]string) map[string]string { } func Activate(sink *os.File, targetFolder string) error { - envCommand := []string{BinRcc(), "internal", "env", "--label", "before"} + envCommand := []string{common.BinRcc(), "internal", "env", "--label", "before"} out, _, err := LiveCapture(targetFolder, envCommand...) if err != nil { return err diff --git a/conda/cleanup.go b/conda/cleanup.go index ef501df8..a6cd2bdb 100644 --- a/conda/cleanup.go +++ b/conda/cleanup.go @@ -61,7 +61,7 @@ func quickCleanup(dryrun bool) error { if dryrun { common.Log("- %v", common.PipCache()) common.Log("- %v", common.HolotreeLocation()) - common.Log("- %v", RobocorpTempRoot()) + common.Log("- %v", common.RobocorpTempRoot()) return nil } safeRemove("cache", common.PipCache()) @@ -69,7 +69,7 @@ func quickCleanup(dryrun bool) error { if err != nil { return err } - return safeRemove("temp", RobocorpTempRoot()) + return safeRemove("temp", common.RobocorpTempRoot()) } func spotlessCleanup(dryrun bool) error { @@ -78,18 +78,18 @@ func spotlessCleanup(dryrun bool) error { return err } if dryrun { - common.Log("- %v", MambaPackages()) + common.Log("- %v", common.MambaPackages()) common.Log("- %v", BinMicromamba()) common.Log("- %v", common.HololibLocation()) return nil } - safeRemove("cache", MambaPackages()) + safeRemove("cache", common.MambaPackages()) safeRemove("executable", BinMicromamba()) return safeRemove("cache", common.HololibLocation()) } func cleanupTemp(deadline time.Time, dryrun bool) error { - basedir := RobocorpTempRoot() + basedir := common.RobocorpTempRoot() handle, err := os.Open(basedir) if err != nil { return err @@ -144,7 +144,7 @@ func Cleanup(daylimit int, dryrun, quick, all, micromamba bool) error { cleanupTemp(deadline, dryrun) if micromamba && err == nil { - err = doCleanup(MambaPackages(), dryrun) + err = doCleanup(common.MambaPackages(), dryrun) } if micromamba && err == nil { err = doCleanup(BinMicromamba(), dryrun) diff --git a/conda/robocorp.go b/conda/robocorp.go index 2d8b9460..2da2304d 100644 --- a/conda/robocorp.go +++ b/conda/robocorp.go @@ -118,10 +118,6 @@ func EnvironmentFor(location string) []string { return append(os.Environ(), EnvironmentExtensionFor(location)...) } -func MambaPackages() string { - return common.ExpandPath(filepath.Join(common.RobocorpHome(), "pkgs")) -} - func asVersion(text string) (uint64, string) { text = strings.TrimSpace(text) multiline := strings.SplitN(text, "\n", 2) @@ -165,12 +161,8 @@ func HasMicroMamba() bool { return goodEnough } -func RobocorpTempRoot() string { - return filepath.Join(common.RobocorpHome(), "temp") -} - func RobocorpTemp() string { - tempLocation := filepath.Join(RobocorpTempRoot(), randomIdentifier) + tempLocation := filepath.Join(common.RobocorpTempRoot(), randomIdentifier) fullpath, err := pathlib.EnsureDirectory(tempLocation) if err != nil { common.Log("WARNING (%v) -> %v", tempLocation, err) @@ -190,13 +182,3 @@ func LocalChannel() (string, bool) { } return "", false } - -func LiveFrom(hash string) string { - // FIXME: remove when base/live is erased - return common.StageFolder -} - -func InstallationPlan(hash string) (string, bool) { - finalplan := filepath.Join(LiveFrom(hash), "rcc_plan.log") - return finalplan, pathlib.IsFile(finalplan) -} diff --git a/conda/workflows.go b/conda/workflows.go index 36357b2c..38738551 100644 --- a/conda/workflows.go +++ b/conda/workflows.go @@ -1,7 +1,6 @@ package conda import ( - "crypto/sha256" "errors" "fmt" "io" @@ -22,23 +21,10 @@ import ( "github.com/robocorp/rcc/xviper" ) -func Hexdigest(raw []byte) string { - return fmt.Sprintf("%02x", raw) -} - func metafile(folder string) string { return common.ExpandPath(folder + ".meta") } -func metaSave(location, data string) error { - // FIXME: remove when base/live is erased - return nil -} - -func LastUsed(location string) (time.Time, error) { - return pathlib.Modtime(metafile(location)) -} - func livePrepare(liveFolder string, command ...string) (*shell.Task, error) { searchPath := FindPath(liveFolder) commandName := command[0] @@ -152,7 +138,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh if force { ttl = "0" } - Progress(5, "Running micromamba phase.") + common.Progress(5, "Running micromamba phase.") mambaCommand := common.NewCommander(BinMicromamba(), "create", "--always-copy", "--no-rc", "--safety-checks", "enabled", "--extra-safety-checks", "--retry-clean-cache", "--strict-channel-priority", "--repodata-ttl", ttl, "-y", "-f", condaYaml, "-p", targetFolder) mambaCommand.Option("--channel-alias", settings.Global.CondaURL()) mambaCommand.ConditionalFlag(common.VerboseEnvironmentBuilding(), "--verbose") @@ -175,9 +161,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh pipUsed, pipCache, wheelCache := false, common.PipCache(), common.WheelCache() size, ok := pathlib.Size(requirementsText) if !ok || size == 0 { - Progress(6, "Skipping pip install phase -- no pip dependencies.") + common.Progress(6, "Skipping pip install phase -- no pip dependencies.") } else { - Progress(6, "Running pip install phase.") + common.Progress(6, "Running pip install phase.") common.Debug("Updating new environment at %v with pip requirements from %v (size: %v)", targetFolder, requirementsText, size) pipCommand := common.NewCommander("pip", "install", "--isolated", "--no-color", "--disable-pip-version-check", "--prefer-binary", "--cache-dir", pipCache, "--find-links", wheelCache, "--requirement", requirementsText) pipCommand.Option("--index-url", settings.Global.PypiURL()) @@ -196,7 +182,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } fmt.Fprintf(planWriter, "\n--- post install plan @%ss ---\n\n", stopwatch) if postInstall != nil && len(postInstall) > 0 { - Progress(7, "Post install scripts phase started.") + common.Progress(7, "Post install scripts phase started.") common.Debug("=== new live --- post install phase ===") for _, script := range postInstall { scriptCommand, err := shlex.Split(script) @@ -214,9 +200,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh } } } else { - Progress(7, "Post install scripts phase skipped -- no scripts.") + common.Progress(7, "Post install scripts phase skipped -- no scripts.") } - Progress(8, "Activate environment started phase.") + common.Progress(8, "Activate environment started phase.") common.Debug("=== new live --- activate phase ===") fmt.Fprintf(planWriter, "\n--- activation plan @%ss ---\n\n", stopwatch) err = Activate(planWriter, targetFolder) @@ -233,10 +219,9 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh fmt.Fprintf(planWriter, "\n--- installation plan complete @%ss ---\n\n", stopwatch) planWriter.Sync() planWriter.Close() - Progress(9, "Update installation plan.") - finalplan, _ := InstallationPlan(key) + common.Progress(9, "Update installation plan.") + finalplan := filepath.Join(targetFolder, "rcc_plan.log") os.Rename(planfile, finalplan) - common.Log("%sInstallation plan is: %v%s", pretty.Yellow, finalplan, pretty.Reset) common.Debug("=== new live --- finalize phase ===") markerFile := filepath.Join(targetFolder, "identity.yaml") @@ -245,12 +230,7 @@ func newLiveInternal(yaml, condaYaml, requirementsText, key string, force, fresh return false, false } - digest, err := DigestFor(targetFolder, nil) - if err != nil { - common.Fatal("Digest", err) - return false, false - } - return metaSave(targetFolder, Hexdigest(digest)) == nil, false + return true, false } func temporaryConfig(condaYaml, requirementsText string, save bool, filenames ...string) (string, string, *Environment, error) { @@ -275,7 +255,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. if err != nil { return "", "", nil, err } - hash := ShortDigest(yaml) + hash := common.ShortDigest(yaml) if !save { return hash, yaml, right, nil } @@ -289,30 +269,7 @@ func temporaryConfig(condaYaml, requirementsText string, save bool, filenames .. return hash, yaml, right, err } -func ShortDigest(content string) string { - digester := sha256.New() - digester.Write([]byte(content)) - result := Hexdigest(digester.Sum(nil)) - return result[:16] -} - -func CalculateComboHash(configurations ...string) (string, error) { - // FIXME: Remove this func once live/base is erased from codebase - key, _, _, err := temporaryConfig("/dev/null", "/dev/null", false, configurations...) - if err != nil { - return "", err - } - return key, nil -} - -func Progress(step int, form string, details ...interface{}) { - message := fmt.Sprintf(form, details...) - common.Log("#### Progress: %d/12 %s %s", step, common.Version, message) - common.Timeline("%d/12 %s", step, message) -} - -func LegacyEnvironment(force bool, configurations ...string) (string, error) { - common.Timeline("New environment.") +func LegacyEnvironment(force bool, configurations ...string) error { cloud.BackgroundMetric(common.ControllerIdentity(), "rcc.env.create.start", common.Version) lockfile := common.RobocorpLock() @@ -321,7 +278,7 @@ func LegacyEnvironment(force bool, configurations ...string) (string, error) { callback() if err != nil { common.Log("Could not get lock on live environment. Quitting!") - return "", err + return err } defer locker.Release() @@ -345,25 +302,24 @@ func LegacyEnvironment(force bool, configurations ...string) (string, error) { if err != nil { failures += 1 xviper.Set("stats.env.failures", failures) - return "", err + return err } defer os.Remove(condaYaml) defer os.Remove(requirementsText) - liveFolder := common.StageFolder success, err := newLive(yaml, condaYaml, requirementsText, key, force, freshInstall, finalEnv.PostInstall) if err != nil { - return "", err + return err } if success { misses += 1 xviper.Set("stats.env.miss", misses) - return liveFolder, nil + return nil } failures += 1 xviper.Set("stats.env.failures", failures) - return "", errors.New("Could not create environment.") + return errors.New("Could not create environment.") } func renameRemove(location string) error { diff --git a/docs/changelog.md b/docs/changelog.md index c228371d..20bf96b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # rcc change log +## v11.0.8 (date: 14.9.2021) UNSTABLE + +- BREAKING CHANGES (ongoing work, see v11.0.0 for more details) +- showing correct `rcc_plan.log` and `identity.yaml` files on log +- reorganizing some common code away from conda module +- rpaframework upgrade to version 11.1.3 in templates + ## v11.0.7 (date: 14.9.2021) UNSTABLE - BREAKING CHANGES (ongoing work, see v11.0.0 for more details) diff --git a/htfs/commands.go b/htfs/commands.go index 2af93fec..3e4667f7 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -24,8 +24,8 @@ func Platform() string { func NewEnvironment(force bool, condafile, holozip string) (label string, err error) { defer fail.Around(&err) - defer conda.Progress(12, "Fresh holotree done.") - conda.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) + defer common.Progress(12, "Fresh holotree done.") + common.Progress(1, "Fresh holotree environment %v.", xviper.TrackingIdentity()) callback := pathlib.LockWaitMessage("Serialized environment creation") locker, err := pathlib.Locker(common.HolotreeLock(), 30000) @@ -38,7 +38,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er _, holotreeBlueprint, err := ComposeFinalBlueprint([]string{condafile}, "") fail.On(err != nil, "%s", err) common.EnvironmentHash = BlueprintHash(holotreeBlueprint) - conda.Progress(2, "Holotree blueprint is %q.", common.EnvironmentHash) + common.Progress(2, "Holotree blueprint is %q.", common.EnvironmentHash) anywork.Scale(100) @@ -60,7 +60,7 @@ func NewEnvironment(force bool, condafile, holozip string) (label string, err er library = tree } - conda.Progress(11, "Restore space from library.") + common.Progress(11, "Restore space from library.") path, err := library.Restore(holotreeBlueprint, []byte(common.ControllerIdentity()), []byte(common.HolotreeSpace)) fail.On(err != nil, "Failed to restore blueprint %q, reason: %v", string(holotreeBlueprint), err) return path, nil @@ -100,22 +100,21 @@ func RecordEnvironment(tree MutableLibrary, blueprint []byte, force bool) (err e common.Debug("Has blueprint environment: %v", exists) if force || !exists { - conda.Progress(3, "Cleanup holotree stage for fresh install.") + common.Progress(3, "Cleanup holotree stage for fresh install.") err = CleanupHolotreeStage(tree) fail.On(err != nil, "Failed to clean stage, reason %v.", err) err = os.MkdirAll(tree.Stage(), 0o755) fail.On(err != nil, "Failed to create stage, reason %v.", err) - conda.Progress(4, "Build environment into holotree stage.") + common.Progress(4, "Build environment into holotree stage.") identityfile := filepath.Join(tree.Stage(), "identity.yaml") err = ioutil.WriteFile(identityfile, blueprint, 0o644) fail.On(err != nil, "Failed to save %q, reason %w.", identityfile, err) - label, err := conda.LegacyEnvironment(force, identityfile) + err = conda.LegacyEnvironment(force, identityfile) fail.On(err != nil, "Failed to create environment, reason %w.", err) - common.Debug("Label: %q", label) - conda.Progress(10, "Record holotree stage to hololib.") + common.Progress(10, "Record holotree stage to hololib.") err = tree.Record(blueprint) fail.On(err != nil, "Failed to record blueprint %q, reason: %w", string(blueprint), err) } diff --git a/htfs/library.go b/htfs/library.go index 2d383186..22c14131 100644 --- a/htfs/library.go +++ b/htfs/library.go @@ -19,6 +19,7 @@ import ( "github.com/robocorp/rcc/fail" "github.com/robocorp/rcc/journal" "github.com/robocorp/rcc/pathlib" + "github.com/robocorp/rcc/pretty" ) const ( @@ -313,6 +314,14 @@ func (it *hololib) Restore(blueprint, client, tag []byte) (result string, err er err = fs.SaveAs(metafile) fail.On(err != nil, "Failed to save metafile %q -> %v", metafile, err) pathlib.TouchWhen(catalog, time.Now()) + planfile := filepath.Join(targetdir, "rcc_plan.log") + if pathlib.FileExist(planfile) { + common.Log("%sInstallation plan is: %v%s", pretty.Yellow, planfile, pretty.Reset) + } + identityfile := filepath.Join(targetdir, "identity.yaml") + if pathlib.FileExist(identityfile) { + common.Log("%sEnvironment configuration descriptor is: %v%s", pretty.Yellow, identityfile, pretty.Reset) + } return targetdir, nil } diff --git a/robot/robot.go b/robot/robot.go index e067448b..02328f03 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -32,7 +32,6 @@ type Robot interface { TaskByName(string) Task UsesConda() bool CondaConfigFile() string - CondaHash() string RootDirectory() string HasHolozip() bool Holozip() string @@ -171,12 +170,7 @@ func (it *robot) Diagnostics(target *common.DiagnosticStatus, production bool) { } target.Details["robot-use-conda"] = fmt.Sprintf("%v", it.UsesConda()) target.Details["robot-conda-file"] = it.CondaConfigFile() - target.Details["robot-conda-hash"] = it.CondaHash() target.Details["hololib.zip"] = it.Holozip() - plan, ok := conda.InstallationPlan(it.CondaHash()) - if ok { - target.Details["robot-conda-plan"] = plan - } target.Details["robot-root-directory"] = it.RootDirectory() target.Details["robot-working-directory"] = it.WorkingDirectory() target.Details["robot-artifact-directory"] = it.ArtifactDirectory() @@ -327,14 +321,6 @@ func (it *robot) CondaConfigFile() string { return filepath.Join(it.Root, it.Conda) } -func (it *robot) CondaHash() string { - result, err := conda.CalculateComboHash(filepath.Join(it.Root, it.Conda)) - if err != nil { - return "" - } - return result -} - func (it *robot) WorkingDirectory() string { return it.Root } diff --git a/robot_tests/export_holozip.robot b/robot_tests/export_holozip.robot index c81130d9..a95bdf18 100644 --- a/robot_tests/export_holozip.robot +++ b/robot_tests/export_holozip.robot @@ -41,18 +41,18 @@ Goal: Must have author space visible Must Have 4e67cd8d4_fcb4b859 Must Have rcc.citests Must Have author - Must Have 2e3ef3ffef58c9ec + Must Have 55aacd3b136421fd Wont Have guest Goal: Show exportable environment list Step build/rcc ht export Use STDERR Must Have Selectable catalogs - Must Have - 2e3ef3ffef58c9ec + Must Have - 55aacd3b136421fd Must Have OK. Goal: Export environment for standalone robot - Step build/rcc ht export -z tmp/standalone/hololib.zip 2e3ef3ffef58c9ec + Step build/rcc ht export -z tmp/standalone/hololib.zip 55aacd3b136421fd Use STDERR Wont Have Selectable catalogs Must Have OK. @@ -75,7 +75,7 @@ Goal: Can delete author space Wont Have 4e67cd8d4_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have 2e3ef3ffef58c9ec + Wont Have 55aacd3b136421fd Wont Have guest Goal: Can run as guest @@ -92,7 +92,7 @@ Goal: No spaces created under guest Wont Have 4e67cd8d4_fcb4b859 Wont Have rcc.citests Wont Have author - Wont Have 2e3ef3ffef58c9ec + Wont Have 55aacd3b136421fd Wont Have 4e67cd8d4_559e19be Wont Have guest @@ -103,6 +103,6 @@ Goal: Space created under author for guest Wont Have 4e67cd8d4_fcb4b859 Wont Have author Must Have rcc.citests - Must Have 2e3ef3ffef58c9ec + Must Have 55aacd3b136421fd Must Have 4e67cd8d4_aacf1552 Must Have guest diff --git a/robot_tests/fullrun.robot b/robot_tests/fullrun.robot index f146f861..9be7ce63 100644 --- a/robot_tests/fullrun.robot +++ b/robot_tests/fullrun.robot @@ -172,7 +172,7 @@ Goal: See variables from specific environment with robot.yaml but without task Must Have PYTHONNOUSERSITE=1 Must Have TEMP= Must Have TMP= - Must Have RCC_ENVIRONMENT_HASH=2e3ef3ffef58c9ec + Must Have RCC_ENVIRONMENT_HASH=55aacd3b136421fd Must Have RCC_INSTALLATION_ID= Must Have RCC_TRACKING_ALLOWED= Must Have PYTHONPATH= diff --git a/templates/extended/conda.yaml b/templates/extended/conda.yaml index a2e9e121..3bcab5e0 100644 --- a/templates/extended/conda.yaml +++ b/templates/extended/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html + - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html diff --git a/templates/python/conda.yaml b/templates/python/conda.yaml index a2e9e121..3bcab5e0 100644 --- a/templates/python/conda.yaml +++ b/templates/python/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html + - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html diff --git a/templates/standard/conda.yaml b/templates/standard/conda.yaml index a2e9e121..3bcab5e0 100644 --- a/templates/standard/conda.yaml +++ b/templates/standard/conda.yaml @@ -12,4 +12,4 @@ dependencies: - pip: # Define pip packages here. # https://pypi.org/ - - rpaframework==10.3.0 # https://rpaframework.org/releasenotes.html + - rpaframework==11.1.3 # https://rpaframework.org/releasenotes.html From b3c436c9871821d0b1874d9f2ea425d4bc202d97 Mon Sep 17 00:00:00 2001 From: Juha Pohjalainen Date: Thu, 16 Sep 2021 11:57:33 +0300 Subject: [PATCH 190/516] RCC-184: removal of base/live environments (v11.1.0) - BREAKING CHANGES, but now this may be considered stable(ish) - micromamba update to version 0.15.3 - added more robot tests and improved `rcc holotree plan` command --- README.md | 8 ++++ cmd/holotreePlan.go | 7 +++- common/version.go | 2 +- conda/platform_darwin_amd64.go | 2 +- conda/platform_linux_amd64.go | 2 +- conda/platform_windows_amd64.go | 2 +- conda/robocorp.go | 2 +- docs/changelog.md | 8 +++- robot_tests/resources.robot | 2 +- robot_tests/templates.robot | 70 +++++++++++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 robot_tests/templates.robot diff --git a/README.md b/README.md index fede29ea..8b70591d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ Together with [robot.yaml](https://robocorp.com/docs/setup/robot-yaml-format) co