From e40494746727751023c082b0fda35a225e073c72 Mon Sep 17 00:00:00 2001 From: Maksim Fedotov Date: Thu, 26 Feb 2026 15:11:57 +0300 Subject: [PATCH] chore(ci): add deckhouse version check Signed-off-by: Maksim Fedotov --- tools/moduleversions/Taskfile.dist.yaml | 26 ++- tools/moduleversions/cmd/moduleversions.go | 25 ++- tools/moduleversions/go.mod | 21 +- tools/moduleversions/go.sum | 58 +++++- .../internal/requirements/checker.go | 189 ++++++++++++++++++ 5 files changed, 296 insertions(+), 23 deletions(-) create mode 100644 tools/moduleversions/internal/requirements/checker.go diff --git a/tools/moduleversions/Taskfile.dist.yaml b/tools/moduleversions/Taskfile.dist.yaml index adff78cd8e..43978f4fdd 100644 --- a/tools/moduleversions/Taskfile.dist.yaml +++ b/tools/moduleversions/Taskfile.dist.yaml @@ -35,9 +35,10 @@ tasks: vars: VERSION: "{{ .VERSION }}" CHANNEL: "{{ .CHANNEL }}" + MODULE: "{{ .MODULE }}" cmds: - | - if [[ -z "{{ .VERSION }}" ]] && [[ -z "{{ .CHANNEL }}" ]]; then + if [[ -z "{{ .VERSION }}" ]] || [[ -z "{{ .CHANNEL }}" ]]; then echo "TAG and CHANNEL are required" exit 1 fi @@ -52,11 +53,20 @@ tasks: local prod_registry="registry.deckhouse.io/deckhouse" local channel=$1 local version=$2 - + local module=$3 + for edition in ${editions[@]}; do + if [[ -z "$module" ]]; then + local url="${prod_registry}/${edition}:${channel}" + local command="crane export $url - | tar -Oxf - deckhouse/version" + else + local url="${prod_registry}/${edition}/modules/$module/release:${channel}" + local command="crane export $url - | tar -Oxf - version.json | jq '.version' -r" + fi echo "Check version on ${edition}" - getVersion=$(crane export "${prod_registry}/${edition}/modules/virtualization/release:${channel}" - | tar -Oxf - version.json | jq '.version' -r) - + getVersion=$( + eval $command + ) if [[ "${getVersion}" != "${version}" ]]; then echo "Version is not valid in ${edition} and channel ${channel}" echo "Expected version: ${version} but got ${getVersion}" @@ -74,14 +84,14 @@ tasks: for i in $(seq 1 $count); do echo "[INFO] Attempt $i/$count..." - check_editions_version {{ .CHANNEL }} {{ .VERSION }} - + check_editions_version {{ .CHANNEL }} {{ .VERSION }} {{ .MODULE }} + if [ "$CHECK" = true ]; then echo "[SUCCESS] Successfully checkd version for editions." success=true break fi - + if [ $i -lt $count ]; then echo "[INFO] Retrying in ${wait_seconds} seconds..." sleep ${wait_seconds} @@ -125,7 +135,7 @@ tasks: for EDITION in ${EDITIONS[@]}; do echo "Check version on $EDITION" getVersion=$(crane export $PROD_REGISTRY/$EDITION/modules/virtualization/release:{{ .CHANNEL }} - | tar -Oxf - version.json | jq '.version' -r) - + if [[ "$getVersion" != "{{ .VERSION }}" ]]; then echo "Version is not valid in $EDITION and channel {{ .CHANNEL }}" echo "Expected version: {{ .VERSION }} but got $getVersion" diff --git a/tools/moduleversions/cmd/moduleversions.go b/tools/moduleversions/cmd/moduleversions.go index 18ecc2775c..537bb051ec 100644 --- a/tools/moduleversions/cmd/moduleversions.go +++ b/tools/moduleversions/cmd/moduleversions.go @@ -23,6 +23,7 @@ import ( "moduleversions/internal/docs" "moduleversions/internal/releases" "moduleversions/internal/version" + "moduleversions/internal/requirements" "github.com/spf13/cobra" ) @@ -33,18 +34,20 @@ const defaultModuleName = "virtualization" func Execute() { rootCmd := NewCommand() if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } } // Config holds the configuration for the command. type Config struct { - Channel string - Version string - ModuleName string - Attempt int - CheckReleases bool - CheckDocs bool + Channel string + Version string + ModuleName string + Attempt int + CheckReleases bool + CheckDocs bool + CheckRequirements bool } var cfg = &Config{} @@ -72,6 +75,7 @@ releases.deckhouse.io (across all editions) and deckhouse.ru/modules/virtualizat rootCmd.Flags().IntVarP(&cfg.Attempt, "attempt", "a", 1, "maximum number of retry attempts") rootCmd.Flags().BoolVarP(&cfg.CheckReleases, "check-releases", "r", false, "check version on releases.deckhouse.io") rootCmd.Flags().BoolVarP(&cfg.CheckDocs, "check-docs", "d", false, "check version on deckhouse.ru/modules/virtualization/[channel]/") + rootCmd.Flags().BoolVarP(&cfg.CheckRequirements, "check-requirements", "q", false, "check module requirements") rootCmd.MarkFlagRequired("channel") rootCmd.MarkFlagRequired("version") @@ -81,7 +85,7 @@ releases.deckhouse.io (across all editions) and deckhouse.ru/modules/virtualizat // Run executes the command logic. func Run(cmd *cobra.Command, args []string) error { - if !cfg.CheckReleases && !cfg.CheckDocs { + if !cfg.CheckReleases && !cfg.CheckDocs && !cfg.CheckRequirements { cfg.CheckReleases = true cfg.CheckDocs = true } @@ -107,6 +111,13 @@ func Run(cmd *cobra.Command, args []string) error { } } + if cfg.CheckRequirements { + err := requirements.CheckVersionWithRetries(normalizedChannel, cfg.Version, cfg.ModuleName, cfg.Attempt) + if err != nil { + hasError = true + } + } + if hasError { return fmt.Errorf("one or more version checks failed") } diff --git a/tools/moduleversions/go.mod b/tools/moduleversions/go.mod index 7302842875..489d866e45 100644 --- a/tools/moduleversions/go.mod +++ b/tools/moduleversions/go.mod @@ -1,16 +1,31 @@ module moduleversions -go 1.24 +go 1.25.6 require ( github.com/PuerkitoBio/goquery v1.10.3 - github.com/spf13/cobra v1.8.1 + github.com/blang/semver/v4 v4.0.0 + github.com/google/go-containerregistry v0.21.1 + github.com/spf13/cobra v1.10.2 golang.org/x/text v0.26.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect + github.com/docker/cli v29.2.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect ) diff --git a/tools/moduleversions/go.sum b/tools/moduleversions/go.sum index 343e3fdc81..53946ceca2 100644 --- a/tools/moduleversions/go.sum +++ b/tools/moduleversions/go.sum @@ -2,16 +2,54 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc= +github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -41,10 +79,13 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -52,6 +93,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -79,5 +122,10 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/tools/moduleversions/internal/requirements/checker.go b/tools/moduleversions/internal/requirements/checker.go new file mode 100644 index 0000000000..93b12fbb98 --- /dev/null +++ b/tools/moduleversions/internal/requirements/checker.go @@ -0,0 +1,189 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package requirements + +import ( + "archive/tar" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "bytes" + "time" + + "gopkg.in/yaml.v3" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/blang/semver/v4" +) + +const ( + moduleFileLinkTemplate = "https://raw.githubusercontent.com/deckhouse/virtualization/refs/tags/%s/module.yaml" + moduleImageURL = "registry.deckhouse.io/deckhouse/%s/modules/%s:%s" + moduleVersionFile = "version.json" + deckhouseImageURL = "registry.deckhouse.io/deckhouse/%s:%s" + deckhouseVersionFile = "deckhouse/version" + httpTimeout = 5 * time.Second +) + +type SemVerRange string +type Modules map[string]SemVerRange +type Requirements struct { + Deckhouse SemVerRange `yaml:"deckhouse"` + Modules Modules `yaml:"modules"` +} +type Config struct { + Requirements Requirements `yaml:"requirements"` +} + +type ModuleVersion struct { + Version string `json:"version"` +} + +func ExtractFileFromImage(image, targetFile string) (string, error) { + ctx := context.Background() // Загружаем образ (аналог crane export) + img, err := crane.Pull(image, crane.WithContext(ctx)) + if err != nil { + return "", fmt.Errorf("pull failed for image %v: %v\n", image, err) + } + var buf bytes.Buffer + err = crane.Export(img, &buf) + if err != nil { + return "", fmt.Errorf("export failed for image %v: %v\n", image, err) + } + + tr := tar.NewReader(&buf) + + for { + hdr, err := tr.Next() + if err == io.EOF { + return "", fmt.Errorf("there is no file %v in tar archive for %v", targetFile, image) + } + if err != nil { + return "", fmt.Errorf("tar read error for image %v: %v\n", image, err) + } + + if hdr.Name == targetFile && hdr.Typeflag == tar.TypeReg { + var buf bytes.Buffer + if _, err := io.Copy(&buf, tr); err != nil { + return "", fmt.Errorf("copy file content: %w", err) + } + return buf.String(), nil + } + } +} + +func VerifyModuleRequirements(module string, sv SemVerRange, edition, channel, tag string) error { + fmt.Printf("semver range of module %s: %s\n", module, sv) + prange, err := semver.ParseRange(string(sv)) + if err != nil { + fmt.Printf("semver.ParseRange failed for module %s: range=%q error=%v\n", module, sv, err) + return fmt.Errorf("failed to parse range for module %v: %w", module, err) + } + + isDeckhouse := false + if module == "deckhouse" { + isDeckhouse = true + } + + var image, tf string + if isDeckhouse { + image = fmt.Sprintf(deckhouseImageURL, edition, channel) + tf = deckhouseVersionFile + } else { + image = fmt.Sprintf(moduleImageURL, edition, module, channel) + tf = moduleVersionFile + } + + file, err := ExtractFileFromImage(image, tf) + if err != nil { + return err + } + + vs := file + if !isDeckhouse { + tmp := ModuleVersion{} + err = json.Unmarshal([]byte(file), &tmp) + if err != nil { + return err + } + vs = tmp.Version + } + fmt.Printf("version of module %s: %s\n", module, file) + version, err := semver.Parse(vs) + if err != nil { + return fmt.Errorf("can't parse module %s version %s", module, version ) + } + if !prange(version) { + return fmt.Errorf("module %s version %s is not in range %s", module, version, sv) + } + return nil +} + +func CheckVersionWithRetries(channel, version, moduleName string, attempts int) error { + client := &http.Client{ + Timeout: httpTimeout, + } + + moduleFileLink := fmt.Sprintf(moduleFileLinkTemplate, version) + fmt.Printf("Fetching module requirements from %s\n", moduleFileLink) + resp, err := client.Get(moduleFileLink) + if err != nil { + fmt.Printf("%v\n", err) + return fmt.Errorf("failed to fetch module file from %s: %w", moduleFileLink, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("unexpected status code %d for URL %s\n", resp.StatusCode, moduleFileLink) + return fmt.Errorf("unexpected status code %d for URL %s\n", resp.StatusCode, moduleFileLink) + } + + file, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("%v\n", err) + return err + } + + c := Config{} + if err := yaml.Unmarshal(file, &c); err != nil { + fmt.Printf("Failed to parse module.yaml: %v\n", err) + return fmt.Errorf("failed to parse module.yaml: %w", err) + } + fmt.Printf("Parsed requirements: deckhouse=%q, modules=%d\n", c.Requirements.Deckhouse, len(c.Requirements.Modules)) + + // skip module check for now + // for k, v := range c.Requirements.Modules { fmt.Printf("Verifying module %s (range %q) on channel %s version %s\n", k, v, channel, version) + // err = VerifyModuleRequirements(k, v, channel, version) + // if err != nil { + // fmt.Printf("verifying module %s on channel %s and version %s failed: %s\n", k, channel, version, err) + // return err + // } + // } + + var supportedEditions = []string{"fe", "ee", "ce", "se-plus"} + for _, e := range supportedEditions { + fmt.Printf("Verifying deckhouse (range %q) on channel %s version %s\n", c.Requirements.Deckhouse, channel, version) + err = VerifyModuleRequirements("deckhouse", c.Requirements.Deckhouse, e, channel, version) + if err != nil { + fmt.Printf("verifying module %s on channel %s and version %s failed: %s\n", "deckhouse", channel, version, err) + return err + } + } + + return nil +}