diff --git a/.gitignore b/.gitignore index ddbac5b896..689093a97b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ test/variables.yaml test/scenario_settings.sh __pycache__ test/dev_overrides.sh +test/bin/microshift-tests diff --git a/scripts/image-builder/configure.sh b/scripts/image-builder/configure.sh index a38bfc4e25..f7f7dbee3e 100755 --- a/scripts/image-builder/configure.sh +++ b/scripts/image-builder/configure.sh @@ -36,6 +36,7 @@ sudo firewall-cmd --add-service=cockpit --permanent "${DNF_RETRY}" "install" "https://dl.fedoraproject.org/pub/epel/epel-release-latest-${OSVERSION}.noarch.rpm" "${DNF_RETRY}" "install" "mock nginx tomcli parallel" sudo usermod -a -G mock "$(whoami)" +sudo usermod -a -G weldr "$(whoami)" # Verify umask and home directory permissions TEST_FILE=$(mktemp /tmp/configure-perm-test.XXXXX) diff --git a/test/bin/build_images.sh b/test/bin/build_images.sh index 8cf5b90e0a..6082cf9882 100755 --- a/test/bin/build_images.sh +++ b/test/bin/build_images.sh @@ -61,55 +61,9 @@ extract_container_images() { } configure_package_sources() { - ## TEMPLATE VARIABLES - export UNAME_M # defined in common.sh - export LOCAL_REPO # defined in common.sh - export NEXT_REPO # defined in common.sh - export BASE_REPO # defined in common.sh - export CURRENT_RELEASE_REPO - export PREVIOUS_RELEASE_REPO - - export SOURCE_VERSION - export FAKE_NEXT_MINOR_VERSION - export MINOR_VERSION - export PREVIOUS_MINOR_VERSION - export YMINUS2_MINOR_VERSION - export SOURCE_VERSION_BASE - export CURRENT_RELEASE_VERSION - export PREVIOUS_RELEASE_VERSION - export YMINUS2_RELEASE_VERSION - export RHOCP_MINOR_Y - export RHOCP_MINOR_Y1 - export RHOCP_MINOR_Y2 - - # Add our sources. It is OK to run these steps repeatedly, if the - # details change they are updated in the service. - title "Expanding package source templates to ${IMAGEDIR}/package-sources" - mkdir -p "${IMAGEDIR}/package-sources" - for template in "${TESTDIR}"/package-sources/*.toml; do - name=$(basename "${template}" .toml) - outfile="${IMAGEDIR}/package-sources/${name}.toml" - - echo "Rendering ${template} to ${outfile}" - ${GOMPLATE} --file "${template}" >"${outfile}" - if [[ "$(wc -l "${outfile}" | cut -d ' ' -f1)" -eq 0 ]]; then - echo "WARNING: Templating '${template}' resulted in empty file! - SKIPPING" - continue - fi - - echo "Adding package source from ${outfile}" - if sudo composer-cli sources list | grep "^${name}\$"; then - sudo composer-cli sources delete "${name}" - fi - sudo composer-cli sources add "${outfile}" - done - - # Show details about the available sources to make debugging easier. - for name in $(sudo composer-cli sources list); do - echo - echo "Package source: ${name}" - sudo composer-cli sources info "${name}" | sed -e 's/gpgkeys.*/gpgkeys = .../g' - done + # `sg weldr` causes command to be run as `weldr` group + # TODO: Add re-log in CI. + sg "weldr" "./bin/microshift-tests compose build" } # Reads release-info RPM for provided version to obtain images diff --git a/test/bin/ci_phase_iso_build.sh b/test/bin/ci_phase_iso_build.sh index a54d321932..3745f290de 100755 --- a/test/bin/ci_phase_iso_build.sh +++ b/test/bin/ci_phase_iso_build.sh @@ -120,6 +120,8 @@ $(dry_run) bash -x ./scripts/image-builder/configure.sh cd "${ROOTDIR}/test/" +go build -o ./bin/microshift-tests ./cmd + # Source common.sh only after all dependencies are installed. # shellcheck source=test/bin/common.sh source "${SCRIPTDIR}/common.sh" diff --git a/test/cmd/main.go b/test/cmd/main.go new file mode 100644 index 0000000000..f2082295df --- /dev/null +++ b/test/cmd/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/openshift/microshift/pkg/util" + compose "github.com/openshift/microshift/test/pkg/compose/cmd" + + "github.com/spf13/cobra" +) + +func main() { + cmd := &cobra.Command{ + Use: "microshift-tests", + Short: "", + SilenceUsage: true, + + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working dir: %w", err) + } + + // Check if app is executed from microshift/test/ directory + varPath := filepath.Join(wd, "..", "Makefile.kube_git.var") + if exists, err := util.PathExists(varPath); err != nil { + return fmt.Errorf("failed checking if %s exists: %w", varPath, err) + } else if !exists { + return fmt.Errorf("could not find %q - microshift-tests must be executed in MicroShift's repository test/", varPath) + } + + return nil + }, + + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + os.Exit(1) + }, + } + + cmd.AddCommand(compose.NewComposeCmd()) + + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000000..01046e23b3 --- /dev/null +++ b/test/go.mod @@ -0,0 +1,51 @@ +module github.com/openshift/microshift/test + +go 1.21.3 + +require github.com/openshift/microshift v0.0.0 + +replace ( + github.com/openshift/microshift => ../ + github.com/openshift/microshift/pkg/util => ../pkg/util +) + +require ( + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 + k8s.io/klog/v2 v2.110.1 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/osbuild/weldr-client/v2 v2.0.0-20240531185649-d81ecc3f6c66 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.11.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.29.1 // indirect + k8s.io/client-go v0.29.1 // indirect + k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 0000000000..fb73620d6b --- /dev/null +++ b/test/go.sum @@ -0,0 +1,142 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cpuguy83/go-md2man/v2 v2.0.3/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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/osbuild/weldr-client/v2 v2.0.0-20240531185649-d81ecc3f6c66 h1:lYcPbmjvmboqrFDlnqjQ5g8DY0loNmB0fZyMWcCp5Vs= +github.com/osbuild/weldr-client/v2 v2.0.0-20240531185649-d81ecc3f6c66/go.mod h1:Lxp9tw/vxkT+dNyM3mvD5xv8EzYZjZv2oSZc+W14wj4= +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.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +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/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/test/pkg/compose/cmd/build.go b/test/pkg/compose/cmd/build.go new file mode 100644 index 0000000000..0896be5b0e --- /dev/null +++ b/test/pkg/compose/cmd/build.go @@ -0,0 +1,77 @@ +package compose + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + uutil "github.com/openshift/microshift/pkg/util" + "github.com/openshift/microshift/test/pkg/compose/helpers" + "github.com/openshift/microshift/test/pkg/compose/sources" + "github.com/openshift/microshift/test/pkg/compose/templatingdata" + "github.com/openshift/microshift/test/pkg/util" + "github.com/spf13/cobra" + "k8s.io/klog/v2" +) + +func newBuildCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "build", + Short: "Run whole pipeline to build images.", + + RunE: func(cmd *cobra.Command, args []string) error { + hostIP, err := uutil.GetHostIP("") + if err != nil { + return err + } + composer, err := helpers.NewComposer(paths, fmt.Sprintf("http://%s:8080/repo", hostIP)) + if err != nil { + return err + } + events := util.NewEventManager("build") + defer func() { + junitFile := filepath.Join(paths.BuildLogsDir, "junit_compose.xml") + junit := events.GetJUnit() + err = junit.WriteToFile(junitFile) + if err != nil { + klog.ErrorS(err, "Failed to write junit to a file", "file", junitFile) + } + + intervalsFile := filepath.Join(paths.BuildLogsDir, "intervals_compose.json") + timelinesFile := filepath.Join(paths.BuildLogsDir, "e2e-timelines_spyglass_compose.html") + err = events.WriteToFiles(intervalsFile, timelinesFile) + if err != nil { + klog.ErrorS(err, "Failed to write events to a files", "file", timelinesFile) + } + }() + + fileSystem := os.DirFS(paths.MicroShiftRepoRootPath) + testFS, err := fs.Sub(fileSystem, "test") + if err != nil { + klog.ErrorS(err, "Failed to get 'test' subFS") + return err + } + sourcesFS, err := fs.Sub(testFS, "package-sources") + if err != nil { + klog.ErrorS(err, "Failed to get 'package-sources' subFS") + return err + } + + opts := &sources.SourceConfigurerOpts{ + Composer: composer, + TplData: templatingdata.NewShim(tplData), + Events: events, + SourcesFS: sourcesFS, + } + sourceConfigurer := sources.SourceConfigurer{Opts: opts} + if err := sourceConfigurer.ConfigureSources(); err != nil { + return err + } + + return nil + }, + } + + return cmd +} diff --git a/test/pkg/compose/cmd/compose.go b/test/pkg/compose/cmd/compose.go new file mode 100644 index 0000000000..6d2170fc4c --- /dev/null +++ b/test/pkg/compose/cmd/compose.go @@ -0,0 +1,64 @@ +package compose + +import ( + "github.com/openshift/microshift/test/pkg/compose/templatingdata" + "github.com/openshift/microshift/test/pkg/util" + "k8s.io/klog/v2" + + "github.com/spf13/cobra" +) + +// Variables common to all compose (sub)commands. Set by `compose` PreRun. +var ( + // Structure containing all relevants filesystem paths so no module needs to calcualate them individually. + paths *util.Paths + + tplData *templatingdata.TemplatingData + + templatingDataFragmentFilepath string + skipContainerImagesExtraction bool +) + +func NewComposeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "compose", + + PersistentPreRunE: composePreRun, + } + + cmd.PersistentFlags().StringVar(&templatingDataFragmentFilepath, + "templating-data", "", + "Path to partial templating data to skip querying remote repository. ") + + cmd.PersistentFlags().BoolVarP(&skipContainerImagesExtraction, + "skip-container-images-extraction", "E", false, + "Skip extraction of images from microshift-release-info RPMs") + + cmd.AddCommand(newTemplatingDataCmd()) + cmd.AddCommand(newBuildCmd()) + + return cmd +} + +func composePreRun(cmd *cobra.Command, args []string) error { + var err error + + paths, err = util.NewPaths() + if err != nil { + return err + } + klog.InfoS("Constructed Paths struct", "paths", paths) + + tplDataOpts := &templatingdata.TemplatingDataOpts{ + Paths: paths, + TemplatingDataFragmentFilepath: templatingDataFragmentFilepath, + SkipContainerImagesExtraction: skipContainerImagesExtraction, + } + tplData, err = tplDataOpts.Construct() + if err != nil { + return err + } + klog.InfoS("Constructed TemplatingData struct", "TemplatingData", tplData) + + return nil +} diff --git a/test/pkg/compose/cmd/templating_data.go b/test/pkg/compose/cmd/templating_data.go new file mode 100644 index 0000000000..9ef287c3ae --- /dev/null +++ b/test/pkg/compose/cmd/templating_data.go @@ -0,0 +1,52 @@ +package compose + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newTemplatingDataCmd() *cobra.Command { + full := false + + cmd := &cobra.Command{ + Use: "templating-data", + Short: "Get templating data with values queried against remotes.", + Long: `Get templating data with values queried against remotes. + +Can be given to other commands to speed up initial phases of build. +For example: + $ microshift-tests compose templating-data > ~/tplData.json + $ microshift-tests compose --templating-data ~/tplData.json TARGET`, + + RunE: func(cmd *cobra.Command, args []string) error { + var output string + var err error + + if full { + // Serialize whole templating data only on demand. + // Primarily for debug: if templating-data-fragment is supplied, + // local values are recalculated anyway because it's cheap and they can change often. + output, err = tplData.String() + } else { + // By default this will only include information that change less often + // and take longer to obtain (i.e. RHOCP and OpenShift mirror related). + output, err = tplData.FragmentString() + } + + if err != nil { + return err + } + + fmt.Printf("%s\n", output) + + return nil + }, + } + + cmd.Flags().BoolVar(&full, + "full", false, + "Obtain full templating data, including local RPM information (source, base, fake)") + + return cmd +} diff --git a/test/pkg/compose/helpers/composer.go b/test/pkg/compose/helpers/composer.go new file mode 100644 index 0000000000..3536cbb9cc --- /dev/null +++ b/test/pkg/compose/helpers/composer.go @@ -0,0 +1,86 @@ +package helpers + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/openshift/microshift/test/pkg/util" + "github.com/osbuild/weldr-client/v2/weldr" + "k8s.io/klog/v2" +) + +type Composer interface { + ListSources() ([]string, error) + DeleteSource(id string) error + AddSource(toml string) error +} + +var _ Composer = (*composer)(nil) + +type composer struct { + client weldr.Client + ostreeRepoURL string + + logsDir string + buildsDir string +} + +func NewComposer(paths *util.Paths, ostreeRepoURL string) (Composer, error) { + client := http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(ostreeRepoURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to %q - is webserver up?", ostreeRepoURL) + } + resp.Body.Close() + + return &composer{ + client: weldr.InitClientUnixSocket(context.Background(), 1, "/run/weldr/api.socket"), + ostreeRepoURL: ostreeRepoURL, + + logsDir: paths.BuildLogsDir, + buildsDir: paths.BuildsDir, + }, nil +} + +func (c *composer) ListSources() ([]string, error) { + klog.InfoS("Listing Composer Sources") + sources, apiResponse, err := c.client.ListSources() + if err != nil { + return nil, fmt.Errorf("listing composer sources failed: %w", err) + } + if apiResponse != nil && !apiResponse.Status { + return nil, fmt.Errorf("ListSources() - wrong api response: %+v", apiResponse) + } + klog.InfoS("Listed Composer Sources", "sources", sources) + return sources, nil +} + +func (c *composer) DeleteSource(id string) error { + klog.InfoS("Deleting Composer Source", "id", id) + apiResponse, err := c.client.DeleteSource(id) + if err != nil { + return fmt.Errorf("deleting composer source failed: %w", err) + } + if apiResponse != nil && !apiResponse.Status { + return fmt.Errorf("DeleteSource(%q) - wrong api response: %+v", id, apiResponse) + } + klog.InfoS("Deleted Composer Source", "id", id) + return nil +} + +func (c *composer) AddSource(toml string) error { + short := strings.ReplaceAll(toml[:50], "\n", "") + "..." + klog.InfoS("Adding Composer Source", "toml", short) + apiResponse, err := c.client.NewSourceTOML(toml) + if err != nil { + return fmt.Errorf("adding composer source failed: %w", err) + } + if apiResponse != nil && !apiResponse.Status { + return fmt.Errorf("NewSourceTOML(...) - wrong api response: %+v", apiResponse) + } + klog.InfoS("Added Composer Source", "toml", short) + return nil +} diff --git a/test/pkg/compose/helpers/composer_dryrun.go b/test/pkg/compose/helpers/composer_dryrun.go new file mode 100644 index 0000000000..8cea8019c9 --- /dev/null +++ b/test/pkg/compose/helpers/composer_dryrun.go @@ -0,0 +1,32 @@ +package helpers + +import ( + "strings" + + "k8s.io/klog/v2" +) + +var _ Composer = (*dryrunComposer)(nil) + +type dryrunComposer struct{} + +func NewDryRunComposer() Composer { + return &dryrunComposer{} +} + +func (c *dryrunComposer) ListSources() ([]string, error) { + sources := []string{} + klog.InfoS("DRYRUN: Listing Sources", "sources", sources) + return sources, nil +} + +func (c *dryrunComposer) DeleteSource(id string) error { + klog.InfoS("DRYRUN: Removing Source", "id", id) + return nil +} + +func (c *dryrunComposer) AddSource(toml string) error { + short := strings.ReplaceAll(toml[:50], "\n", "") + "..." + klog.InfoS("DRYRUN: Adding Source", "toml", short) + return nil +} diff --git a/test/pkg/compose/sources/sources.go b/test/pkg/compose/sources/sources.go new file mode 100644 index 0000000000..ba0da96e8f --- /dev/null +++ b/test/pkg/compose/sources/sources.go @@ -0,0 +1,126 @@ +package sources + +import ( + "errors" + "fmt" + "io/fs" + "slices" + "time" + + "github.com/openshift/microshift/test/pkg/compose/helpers" + "github.com/openshift/microshift/test/pkg/compose/templatingdata" + "github.com/openshift/microshift/test/pkg/util" + + "k8s.io/klog/v2" +) + +type SourceConfigurerOpts struct { + Composer helpers.Composer + TplData *templatingdata.Shim + Events util.EventManager + SourcesFS fs.FS +} + +type SourceConfigurer struct { + Opts *SourceConfigurerOpts +} + +func (sc *SourceConfigurer) ConfigureSources() error { + existingSources, err := sc.Opts.Composer.ListSources() + if err != nil { + return err + } + + entries, err := fs.ReadDir(sc.Opts.SourcesFS, ".") + if err != nil { + return err + } + + errs := []error{} + + for _, entry := range entries { + if entry.IsDir() { + klog.InfoS("Ignoring unexpected dir in package-sources/", "name", entry.Name()) + continue + } + + if err := sc.processSource(entry.Name(), existingSources); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +func (sc *SourceConfigurer) processSource(filename string, existingSources []string) error { + start := time.Now() + + dataBytes, err := fs.ReadFile(sc.Opts.SourcesFS, filename) + if err != nil { + return fmt.Errorf("failed to read %q: %w", filename, err) + } + + data := string(dataBytes) + + // Get source name/id directly from the TOML file to not operate on assumption + // that filename without extension is name of the composer Source. + name, err := util.GetTOMLFieldValue(data, "id") + if err != nil { + return err + } + + result, err := sc.Opts.TplData.Template(name, data) + if err != nil { + return err + } + + if len(result) == 0 { + if slices.Contains(existingSources, name) { + klog.InfoS("Template is empty but exists in composer - removing", "name", name) + if err := sc.Opts.Composer.DeleteSource(name); err != nil { + klog.ErrorS(err, "Deleting composer source failed") + return err + } + } else { + klog.InfoS("Template is empty - not adding", "name", name) + sc.Opts.Events.AddEvent(&util.SkippedEvent{ + Event: util.Event{ + Name: name, + Suite: "sources", + ClassName: "source", + Start: start, + End: time.Now(), + }, + Message: "Empty result of templating", + }) + } + return nil + } + + klog.InfoS("Adding source to the composer", "name", name) + if err := sc.Opts.Composer.AddSource(result); err != nil { + klog.ErrorS(err, "Adding composer source failed") + sc.Opts.Events.AddEvent(&util.FailedEvent{ + Event: util.Event{ + Name: name, + Suite: "sources", + ClassName: "source", + Start: start, + End: time.Now(), + }, + Message: "Adding composer source failed", + Content: err.Error(), + }) + return err + } + + sc.Opts.Events.AddEvent(&util.Event{ + Name: name, + Suite: "sources", + ClassName: "source", + Start: start, + End: time.Now(), + }) + + return nil +} diff --git a/test/pkg/compose/sources/sources_test.go b/test/pkg/compose/sources/sources_test.go new file mode 100644 index 0000000000..ab05e03272 --- /dev/null +++ b/test/pkg/compose/sources/sources_test.go @@ -0,0 +1,334 @@ +package sources + +import ( + "fmt" + "reflect" + "testing" + "testing/fstest" + + "github.com/openshift/microshift/test/pkg/compose/templatingdata" + "github.com/openshift/microshift/test/pkg/util" + "github.com/openshift/microshift/test/pkg/util/mocks" + + "github.com/BurntSushi/toml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func Test_SuccessfulFlow(t *testing.T) { + sourcesFS := fstest.MapFS{ + "microshift-base.toml": &fstest.MapFile{ + Data: []byte(`id = "microshift-base" +name = "MicroShift {{ .Base.Repository }} Repo" +type = "yum-baseurl" +url = "file://{{ .Base.Repository }}/" +check_gpg = false +check_ssl = false +system = false`)}, + + "microshift-crel.toml": &fstest.MapFile{ + Data: []byte(`{{- if hasPrefix .Current.Repository "http" -}} +id = "microshift-crel" +name = "Repository with already existing RPMs for current release" +type = "yum-baseurl" +url = "{{ .Current.Repository }}" +check_gpg = false +check_ssl = true +system = false +{{- end -}}`)}, + + "microshift-external.toml": &fstest.MapFile{ + Data: []byte(`{{- if .External.Repository -}} +id = "microshift-external" +name = "Repository with externally supplied RPMs" +type = "yum-baseurl" +url = "{{ .External.Repository }}" +check_gpg = false +check_ssl = true +system = false +{{- end -}}`)}, + + "microshift-local.toml": &fstest.MapFile{ + Data: []byte(`id = "microshift-local" +name = "MicroShift Local Repo" +type = "yum-baseurl" +url = "file://{{ .Source.Repository }}/" +check_gpg = false +check_ssl = false +system = false`)}, + + "microshift-prel.toml": &fstest.MapFile{ + Data: []byte(`{{- if hasPrefix .Previous.Repository "http" -}} +id = "microshift-prel" +name = "Repository with RPMs for previous release" +type = "yum-baseurl" +url = "{{ .Previous.Repository }}" +check_gpg = false +check_ssl = true +system = false +{{- end -}}`)}, + + "rhocp-y.toml": &fstest.MapFile{ + Data: []byte(`{{- if .RHOCPMinorY -}} +id = "rhocp-y" +name = "Red Hat OpenShift Container Platform 4.{{ .RHOCPMinorY }} for RHEL 9" +type = "yum-baseurl" +url = "https://cdn.redhat.com/content/dist/layered/rhel9/{{ .Arch }}/rhocp/4.{{ .RHOCPMinorY }}/os" +check_gpg = true +check_ssl = true +system = false +rhsm = true +{{- end -}}`)}, + + "rhocp-y1.toml": &fstest.MapFile{ + Data: []byte(`{{- if .RHOCPMinorY1 -}} +id = "rhocp-y1" +name = "Red Hat OpenShift Container Platform 4.{{ .RHOCPMinorY1 }} for RHEL 9" +type = "yum-baseurl" +url = "https://cdn.redhat.com/content/dist/layered/rhel9/{{ .Arch }}/rhocp/4.{{ .RHOCPMinorY1 }}/os" +check_gpg = true +check_ssl = true +system = false +rhsm = true +{{- end -}}`)}, + "rhocp-y2.toml": &fstest.MapFile{ + Data: []byte(`{{- if .RHOCPMinorY2 -}} +id = "rhocp-y2" +name = "Red Hat OpenShift Container Platform 4.{{ .RHOCPMinorY2 }} for RHEL 9" +type = "yum-baseurl" +url = "https://cdn.redhat.com/content/dist/layered/rhel9/{{ .Arch }}/rhocp/4.{{ .RHOCPMinorY2 }}/os" +check_gpg = true +check_ssl = true +system = false +rhsm = true +{{- end -}}`)}, + } + + composer := new(mocks.ComposerMock) + events := new(mocks.EventManagerMock) + + baseRepo := "/path/to/base/rpms" + crelRepo := "http://fake-repository.com" + prevRepo := "rhocp-4.15" + srcRepo := "/home/microshift/_output/local" + extRepo := "/some/path" + arch := "x86_64" + rhocpY := 0 + rhocpY1 := 15 + rhocpY2 := 14 + + opts := &SourceConfigurerOpts{ + Composer: composer, + TplData: templatingdata.NewShim(&templatingdata.TemplatingData{ + Arch: arch, + RHOCPMinorY: rhocpY, + RHOCPMinorY1: rhocpY1, + RHOCPMinorY2: rhocpY2, + Current: templatingdata.Release{ + Repository: crelRepo, + }, + Previous: templatingdata.Release{ + Repository: prevRepo, // because it doesn't start with `http`, the rendered Source will be empty + }, + Base: templatingdata.Release{ + Repository: baseRepo, + }, + Source: templatingdata.Release{ + Repository: srcRepo, + }, + External: templatingdata.Release{ + // non-empty value will cause microshift-ext.toml to be templated successfully and added to the composer + Repository: extRepo, + }, + }), + Events: events, + SourcesFS: sourcesFS, + } + + getRHCOPUrl := func(minor int) string { + return fmt.Sprintf("https://cdn.redhat.com/content/dist/layered/rhel9/%s/rhocp/4.%d/os", arch, minor) + } + + composer.On("ListSources").Return([]string{"rhocp-y", "rhocp-y2"}, nil) + + // microshift-base + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-base"), + UrlShouldBe(t, fmt.Sprintf("file://%s/", baseRepo)), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-base", &util.Event{}))).Once() + + // microshift-crel + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-crel"), + UrlShouldBe(t, crelRepo), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-crel", &util.Event{}))).Once() + + // microshift-external + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-external"), + UrlShouldBe(t, extRepo), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-external", &util.Event{}))).Once() + + // microshift-local + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-local"), + UrlShouldBe(t, fmt.Sprintf("file://%s/", srcRepo)), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-local", &util.Event{}))).Once() + + // microshift-prel - should not cause AddSource to be called, but AddEvent with SkippedEvent should be called + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-prel", &util.SkippedEvent{}))).Once() + + // rhocp-y + // Present in ListSources(), but templated to empty string because of the .RHOCPY=0: Delete, don't Add + composer.On("DeleteSource", "rhocp-y").Return(nil).Once() + + // rhocp-y1 + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "rhocp-y1"), + UrlShouldBe(t, getRHCOPUrl(rhocpY1)), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("rhocp-y1", &util.Event{}))).Once() + + // rhocp-y2 + // Although it's already present in the composer (present in slice returned from ListSources()), it's not deleted, just updated with AddEvent call + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "rhocp-y2"), + UrlShouldBe(t, getRHCOPUrl(rhocpY2)), + ))).Return(nil).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("rhocp-y2", &util.Event{}))).Once() + + err := (&SourceConfigurer{Opts: opts}).ConfigureSources() + assert.NoError(t, err) + composer.AssertExpectations(t) + events.AssertExpectations(t) +} + +// Verify that error from AddSource() is both: registered in the Events and propagated back to the caller. +func Test_AddSourceFailed(t *testing.T) { + sourcesFS := fstest.MapFS{ + "microshift-base.toml": &fstest.MapFile{ + Data: []byte(`id = "microshift-base" +name = "MicroShift {{ .Base.Repository }} Repo" +type = "yum-baseurl" +url = "file://{{ .Base.Repository }}/" +check_gpg = false +check_ssl = false +system = false`)}, + "microshift-crel.toml": &fstest.MapFile{ + Data: []byte(`{{- if hasPrefix .Current.Repository "http" -}} +id = "microshift-crel" +name = "Repository with already existing RPMs for current release" +type = "yum-baseurl" +url = "{{ .Current.Repository }}" +check_gpg = false +check_ssl = true +system = false +{{- end -}}`)}, + } + + composer := new(mocks.ComposerMock) + events := new(mocks.EventManagerMock) + + baseRepo := "/path/to/base/rpms" + crelRepo := "http://fake-repository.com" + + opts := &SourceConfigurerOpts{ + Composer: composer, + TplData: templatingdata.NewShim(&templatingdata.TemplatingData{ + Base: templatingdata.Release{ + Repository: baseRepo, + }, + Current: templatingdata.Release{ + Repository: crelRepo, + }, + }), + Events: events, + SourcesFS: sourcesFS, + } + + composer.On("ListSources").Return([]string{"rhocp-y", "rhocp-y2"}, nil) + + // microshift-base + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-base"), + UrlShouldBe(t, fmt.Sprintf("file://%s/", baseRepo)), + ))).Return(fmt.Errorf("some error")).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-base", &util.FailedEvent{}))).Once() + + // microshift-crel + composer.On("AddSource", + mock.MatchedBy(And( + IdShouldBe(t, "microshift-crel"), + UrlShouldBe(t, crelRepo), + ))).Return(fmt.Errorf("some other error")).Once() + events.On("AddEvent", mock.MatchedBy(EventShouldBe("microshift-crel", &util.FailedEvent{}))).Once() + + err := (&SourceConfigurer{Opts: opts}).ConfigureSources() + assert.Error(t, err) + assert.ErrorContains(t, err, "some error") + assert.ErrorContains(t, err, "some other error") + composer.AssertExpectations(t) + events.AssertExpectations(t) +} + +func EventShouldBe(expectedName string, expectedType util.IEvent) func(util.IEvent) bool { + return func(ev util.IEvent) bool { + if ev.GetName() != expectedName { + return false + } + + if reflect.TypeOf(ev) != reflect.TypeOf(expectedType) { + return false + } + + return true + } +} + +func And(preds ...func(string) bool) func(string) bool { + return func(s string) bool { + for _, pred := range preds { + if !pred(s) { + return false + } + } + return true + } +} + +func IdShouldBe(t *testing.T, id string) func(string) bool { + return func(tomlSource string) bool { + src := Source{} + _, err := toml.Decode(tomlSource, &src) + assert.NoError(t, err) + return src.Id == id + } +} + +func UrlShouldBe(t *testing.T, url string) func(string) bool { + return func(tomlSource string) bool { + src := Source{} + _, err := toml.Decode(tomlSource, &src) + assert.NoError(t, err) + return src.Url == url + } +} + +// Source is a brief representation of composer's Source. +// Added here to not import more dependencies as we know exactly what fields we use and care about in tests. +type Source struct { + Id string + Name string + Url string +} diff --git a/test/pkg/compose/templatingdata/constructor.go b/test/pkg/compose/templatingdata/constructor.go new file mode 100644 index 0000000000..b10c4e9779 --- /dev/null +++ b/test/pkg/compose/templatingdata/constructor.go @@ -0,0 +1,397 @@ +package templatingdata + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/openshift/microshift/pkg/util" + testutil "github.com/openshift/microshift/test/pkg/util" + "k8s.io/klog/v2" +) + +var ( + releaseInfoRpmRx = regexp.MustCompile("^microshift-release-info-.*.rpm$") + errNoRemoteRelease = fmt.Errorf("release from remote repo not found") +) + +type TemplatingDataOpts struct { + Paths *testutil.Paths + TemplatingDataFragmentFilepath string + SkipContainerImagesExtraction bool +} + +func (o *TemplatingDataOpts) Construct() (*TemplatingData, error) { + klog.InfoS("Constructing TemplatingData") + + localRepo := path.Join(o.Paths.RPMRepos, "microshift-local") + fakeNextRepo := path.Join(o.Paths.RPMRepos, "microshift-fake-next-minor") + baseRepo := path.Join(o.Paths.RPMRepos, "microshift-base") + externalRepo := path.Join(o.Paths.RPMRepos, "microshift-external") + + if exists, err := util.PathExists(localRepo); err != nil { + return nil, fmt.Errorf("failed to check if %s exists: %w", localRepo, err) + } else if !exists { + return nil, fmt.Errorf("%s does not exist - did you run build_rpms.sh and create_local_repos.sh?", localRepo) + } + + var td *TemplatingData + var err error + + if o.TemplatingDataFragmentFilepath != "" { + td, err = unmarshalTemplatingData(o.TemplatingDataFragmentFilepath) + if err != nil { + return nil, err + } + klog.InfoS("TemplatingData fragment was provided and loaded", "intermediateTeplatingData", td) + } else { + td = &TemplatingData{} + } + + td.Arch = getArch() + + if td.Source.Repository == "" { + td.Source, err = o.getReleaseFromLocalFs(localRepo) + if err != nil { + return nil, err + } + } + + if td.Base.Repository == "" { + td.Base, err = o.getReleaseFromLocalFs(baseRepo) + if err != nil { + return nil, err + } + } + + if td.FakeNext.Repository == "" { + td.FakeNext, err = o.getReleaseFromLocalFs(fakeNextRepo) + if err != nil { + return nil, err + } + } + + if td.External.Repository == "" { + exists, err := util.PathExistsAndIsNotEmpty(externalRepo) + if err != nil { + return nil, err + } + if exists { + td.External, err = o.getReleaseFromLocalFs(externalRepo) + if err != nil { + return nil, err + } + } + } + + if td.Current.Repository == "" { + td.Current, err = o.getReleaseFromRemoteRepo(td.Source.Minor) + if err != nil && !errors.Is(err, errNoRemoteRelease) { + return nil, err + } + } + + if td.Previous.Repository == "" { + td.Previous, err = o.getReleaseFromRemoteRepo(td.Source.Minor - 1) + if err != nil { + return nil, err + } + } + + if td.YMinus2.Repository == "" { + td.YMinus2, err = o.getReleaseFromRemoteRepo(td.Source.Minor - 2) + if err != nil { + return nil, err + } + } + + // If templatingDataInputPath was provided, assume the 0 is + // already "calculated" value and repo is not available yet. + if td.RHOCPMinorY == 0 && o.TemplatingDataFragmentFilepath == "" { + if isRHOCPAvailable(td.Source.Minor) { + td.RHOCPMinorY = td.Source.Minor + } + } + + if td.RHOCPMinorY1 == 0 { + if isRHOCPAvailable(td.Previous.Minor) { + td.RHOCPMinorY1 = td.Previous.Minor + } + } + + if td.RHOCPMinorY2 == 0 { + if isRHOCPAvailable(td.YMinus2.Minor) { + td.RHOCPMinorY2 = td.YMinus2.Minor + } + } + + klog.InfoS("Constructed TemplatingData", "results", td) + + return td, nil +} + +func unmarshalTemplatingData(path string) (*TemplatingData, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file %q: %w", path, err) + } + + td := &TemplatingData{} + + err = json.Unmarshal(data, td) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal partial templating data from %q: %w", path, err) + } + + return td, nil +} + +func getArch() string { + if runtime.GOARCH == "amd64" { + return "x86_64" + } + return "aarch64" +} + +// getReleaseFromLocalFs creates `Release` from local filesystem repository +func (o *TemplatingDataOpts) getReleaseFromLocalFs(repo string) (Release, error) { + releaseInfoFile := "" + err := filepath.WalkDir(repo, func(path string, d fs.DirEntry, err error) error { + if releaseInfoFile != "" { + return nil + } + + if err != nil { + klog.ErrorS(err, "Error during WalkDir - ignoring") + return nil + } + + if releaseInfoRpmRx.MatchString(d.Name()) { + releaseInfoFile = path + } + return nil + }) + if err != nil { + return Release{}, err + } + + if releaseInfoFile == "" { + return Release{}, fmt.Errorf("could not find microshift-release-info RPM in directory %q", repo) + } + klog.InfoS("Found microshift-release-info RPM for local repository", "repo", repo) + + rpmVersion, _, err := testutil.RunCommand("rpm", "-q", "--queryformat", "%{version}", releaseInfoFile) + if err != nil { + return Release{}, fmt.Errorf("failed to get version of the microshift-release-info (%q) RPM: %w", releaseInfoFile, err) + } + + minorStr := strings.Split(rpmVersion, ".")[1] + minor, err := strconv.Atoi(minorStr) + if err != nil { + return Release{}, fmt.Errorf("failed to convert %q to int: %w", minorStr, err) + } + + var images []string + if o.SkipContainerImagesExtraction { + klog.InfoS("Skipping container image extraction", "version", rpmVersion) + } else { + images, err = getContainerImages(releaseInfoFile) + if err != nil { + return Release{}, err + } + } + + return Release{ + Repository: repo, + Version: rpmVersion, + Minor: minor, + Images: images, + }, nil +} + +// getReleaseFromRemoteRepo creates `Release` from remote repository. +// It looks for MicroShift RPM in following order: +// RHOCP, Release Candidates on OpenShift mirror, Engineering Candidates on OpenShift mirror. +func (o *TemplatingDataOpts) getReleaseFromRemoteRepo(minor int) (Release, error) { + klog.InfoS("Looking for a Release for minor version", "minor", minor) + + r, err := o.getReleaseFromRHOCP(minor) + if err != nil && !errors.Is(err, errNoRemoteRelease) { + return Release{}, err + } + if err == nil { + klog.InfoS("Found release in RHOCP repository", "minor", minor, "release", r) + return r, nil + } + + r, err = o.getReleaseFromTheMirror(minor, false) + if err != nil && !errors.Is(err, errNoRemoteRelease) { + return Release{}, err + } + if err == nil { + klog.InfoS("Found release in RC mirror", "minor", minor, "release", r) + return r, nil + } + + r, err = o.getReleaseFromTheMirror(minor, true) + if err != nil && !errors.Is(err, errNoRemoteRelease) { + return Release{}, err + } + if err == nil { + klog.InfoS("Found release in EC mirror", "minor", minor, "release", r) + return r, nil + } + + klog.InfoS("No RPMs for the minor version found", "minor", minor) + + return Release{}, errNoRemoteRelease +} + +// getReleaseFromTheMirror looks for MicroShift RPM in OpenShift mirror +func (o *TemplatingDataOpts) getReleaseFromTheMirror(minor int, devPreview bool) (Release, error) { + dp := "" + if devPreview { + dp = "-dev-preview" + } + + repo := fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/%s/microshift/ocp%s/latest-4.%d/el9/os/", getArch(), dp, minor) + + resp, err := http.Get(repo) + if err != nil { + return Release{}, fmt.Errorf("http.Get(%s) failed: %w", repo, err) + } + if resp.StatusCode != 200 { // TODO: Maybe this should compare to 404? + return Release{}, errNoRemoteRelease + } + version, _, err := testutil.RunCommand( + "sudo", "dnf", "repoquery", "microshift", "--quiet", + "--queryformat", "%{version}-%{release}", + "--disablerepo", "*", + "--repofrompath", fmt.Sprintf("this,%s", repo), + ) + if err != nil { + return Release{}, fmt.Errorf("failed to repoquery %q for microshift RPM: %w", repo, err) + } + var images []string + if o.SkipContainerImagesExtraction { + klog.InfoS("Skipping container image extraction", "version", version) + } else { + relInfo, err := downloadReleaseInfoRPM(version, "--repofrompath", fmt.Sprintf("this,%s", repo)) + if err != nil { + return Release{}, err + } + images, err = getContainerImages(relInfo) + if err != nil { + return Release{}, err + } + } + + return Release{ + Repository: repo, + Version: version, + Minor: minor, + Images: images, + }, nil +} + +// getReleaseFromRHOCP looks for MicroShift RPM in RHOCP +func (o *TemplatingDataOpts) getReleaseFromRHOCP(minor int) (Release, error) { + rhocp := fmt.Sprintf("rhocp-4.%d-for-rhel-9-%s-rpms", minor, getArch()) + version, serr, err := testutil.RunCommand("sudo", "dnf", "repoquery", "microshift", + "--quiet", + "--queryformat", "%{version}-%{release}", + "--repo", rhocp, + "--latest-limit", "1", + ) + if err == nil { + var images []string + if o.SkipContainerImagesExtraction { + klog.InfoS("Skipping container image extraction", "version", version) + } else { + relInfo, err := downloadReleaseInfoRPM(version, "--repo", rhocp) + if err != nil { + return Release{}, err + } + images, err = getContainerImages(relInfo) + if err != nil { + return Release{}, err + } + } + return Release{ + Repository: rhocp, + Version: version, + Minor: minor, + Images: images, + }, nil + } + + if strings.Contains(serr, fmt.Sprintf("Error: Unknown repo: '%s'", rhocp)) { + return Release{}, errNoRemoteRelease + } + + if strings.Contains(serr, "Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried") { + return Release{}, errNoRemoteRelease + } + + return Release{}, err +} + +func downloadReleaseInfoRPM(version string, repoOpts ...string) (string, error) { + klog.InfoS("Downloading microshift-release-info", "version", version) + + destDir := "/tmp" + cmd := append([]string{"sudo", "dnf", "download", fmt.Sprintf("microshift-release-info-%s", version), + "--destdir", "/tmp"}, repoOpts...) + _, _, err := testutil.RunCommand(cmd...) + if err != nil { + return "", fmt.Errorf("failed to download %q RPM: %w", fmt.Sprintf("microshift-release-info-%s", version), err) + } + + // Because of `dnf download` superfluous output we cannot use the stdout. + // Using --quiet would also cause RPM name to not be printed. + path := filepath.Join(destDir, "microshift-release-info-"+version+".noarch.rpm") + klog.InfoS("Downloaded microshift-release-info", "version", version, "destination", path) + return path, nil +} + +// isRHOCPAvailable checks if RHOCP of a given `minor` is available for usage by attempting +// to query the repository for cri-o package +func isRHOCPAvailable(minor int) bool { + repo := fmt.Sprintf("rhocp-4.%d-for-rhel-9-%s-rpms", minor, getArch()) + _, _, err := testutil.RunCommand("sudo", "dnf", "repository-packages", repo, "info", "cri-o") + return err == nil +} + +// getContainerImages extracts list of images from release.json file inside given release-info RPM +func getContainerImages(releaseInfoFilePath string) ([]string, error) { + sout, _, err := testutil.RunCommand( + "bash", "-c", + fmt.Sprintf("rpm2cpio %s | cpio -i --to-stdout '*release-%s.json'", releaseInfoFilePath, getArch()), + ) + if err != nil { + return []string{}, fmt.Errorf("failed to obtain images from a %q: %w", releaseInfoFilePath, err) + } + + data := struct { + Images map[string]string + }{} + if err := json.Unmarshal([]byte(sout), &data); err != nil { + return []string{}, fmt.Errorf("failed to unmarshal images from a %q: %w", releaseInfoFilePath, err) + } + + images := []string{} + for _, image := range data.Images { + images = append(images, image) + } + + return images, nil +} diff --git a/test/pkg/compose/templatingdata/shim.go b/test/pkg/compose/templatingdata/shim.go new file mode 100644 index 0000000000..97cee50538 --- /dev/null +++ b/test/pkg/compose/templatingdata/shim.go @@ -0,0 +1,94 @@ +package templatingdata + +import ( + "fmt" + "strconv" + "strings" + "text/template" + + "k8s.io/klog/v2" +) + +// Shim is a struct that works with legacy templates using gomplate +type Shim struct { + Env map[string]string +} + +func NewShim(tplData *TemplatingData) *Shim { + s := &Shim{ + Env: map[string]string{ + "UNAME_M": tplData.Arch, + + "LOCAL_REPO": tplData.Source.Repository, + "NEXT_REPO": tplData.FakeNext.Repository, + "BASE_REPO": tplData.Base.Repository, + "CURRENT_RELEASE_REPO": tplData.Current.Repository, + "PREVIOUS_RELEASE_REPO": tplData.Previous.Repository, + + "FAKE_NEXT_MINOR_VERSION": strconv.Itoa(tplData.FakeNext.Minor), + "MINOR_VERSION": strconv.Itoa(tplData.Source.Minor), + "PREVIOUS_MINOR_VERSION": strconv.Itoa(tplData.Previous.Minor), + "YMINUS2_MINOR_VERSION": strconv.Itoa(tplData.YMinus2.Minor), + + "SOURCE_VERSION": tplData.Source.Version, + "SOURCE_VERSION_BASE": tplData.Base.Version, + "CURRENT_RELEASE_VERSION": tplData.Current.Version, + "PREVIOUS_RELEASE_VERSION": tplData.Previous.Version, + "YMINUS2_RELEASE_VERSION": tplData.YMinus2.Version, + + "SOURCE_IMAGES": strings.Join(tplData.Source.Images, ","), + }, + } + + if tplData.RHOCPMinorY != 0 { + s.Env["RHOCP_MINOR_Y"] = strconv.Itoa(tplData.RHOCPMinorY) + } + if tplData.RHOCPMinorY1 != 0 { + s.Env["RHOCP_MINOR_Y1"] = strconv.Itoa(tplData.RHOCPMinorY1) + } + if tplData.RHOCPMinorY2 != 0 { + s.Env["RHOCP_MINOR_Y2"] = strconv.Itoa(tplData.RHOCPMinorY2) + } + + return s +} + +type FakeEnv struct { + s *Shim +} + +func (fe *FakeEnv) Getenv(k string, def string) string { + v, ok := fe.s.Env[k] + if !ok { + return def + } + return v +} + +func (s *Shim) Template(name, data string) (string, error) { + klog.InfoS("Templating input text", "template", name, "preTemplating", data) + + fe := &FakeEnv{s: s} + funcs := map[string]any{ + "hasPrefix": strings.HasPrefix, + "env": func() interface{} { return fe }, + } + + tpl, err := template.New(name).Funcs(funcs).Parse(data) + if err != nil { + klog.ErrorS(err, "Failed to parse template file", "template", name) + return "", fmt.Errorf("failed to parse template %q: %w", name, err) + } + + b := &strings.Builder{} + err = tpl.Execute(b, s) + if err != nil { + klog.ErrorS(err, "Executing template failed", "template", name) + return "", fmt.Errorf("failed to execute template %q: %w", name, err) + } + + result := b.String() + klog.InfoS("Templating successful", "template", name, "postTemplating", result) + + return result, nil +} diff --git a/test/pkg/compose/templatingdata/types.go b/test/pkg/compose/templatingdata/types.go new file mode 100644 index 0000000000..95041fec71 --- /dev/null +++ b/test/pkg/compose/templatingdata/types.go @@ -0,0 +1,121 @@ +package templatingdata + +import ( + "encoding/json" + "fmt" + "strings" + "text/template" + + "k8s.io/klog/v2" +) + +// TemplatingData contains all values needed for templating Composer's Sources & Blueprints, +// and other templated artifacts within a MicroShift's test harness. +type TemplatingData struct { + Arch string + + // Minor version of current release's RHOCP. If RHOCP is not available yet, it defaults to 0. + RHOCPMinorY int + + // Minor version of previous release's RHOCP. If RHOCP is not available yet, it defaults to 0. + RHOCPMinorY1 int + + // Minor version of previous previous release's RHOCP. + RHOCPMinorY2 int + + // Current stores metadata of current release, i.e. matching currently checked out git branch. + // If the RHOCP is not available yet, it can point to Release or Engineering candidates present on the OpenShift mirror. + // If those are also not available, it will be empty and related composer Sources and Blueprints will not be build. + Current Release + + // Current stores metadata of current release, i.e. matching currently checked out git branch. + // If the RHOCP is not available yet, it can point to Release or Engineering candidates present on the OpenShift mirror. + Previous Release + + // Current stores metadata of current release, i.e. matching currently checked out git branch. + // Usually this should always point to RHOCP because it's two minor versions older than what we're working on currently. + YMinus2 Release + + // Source stores metadata of RPMs built from currently checked out source code. + Source Release + + // Source stores metadata of RPMs built from base branch of currently checked out branch. + // Usually it can be `main` or `release-4.Y`. + Base Release + + // Source stores metadata of RPMs built from currently checked out source code with minor version overridden + // to be newer than what we're currently working on. + // These are needed for various ostree upgrade tests. + FakeNext Release + + // External stores metadata of RPMs supplied from external source, like private builds. + External Release +} + +// Release represents metadata of particular set of RPMs. +type Release struct { + // Repository is where RPM resides. It can be local (on the disk), http (like OpenShift's mirror), or RHOCP. + Repository string + + // Version is full version string of a RPM, e.g. 4.14.16-202403071942.p0.g4cef5f2.assembly.4.14.16.el9. + Version string + + // Minor is minor part of the version, e.g. 14. + Minor int + + // Images is a list of images stored in release-info RPM. + // Currently only for local repositories. + Images []string +} + +func (td *TemplatingData) Template(name, data string) (string, error) { + klog.InfoS("Templating input text", "template", name, "preTemplating", data) + + funcs := map[string]any{ + "hasPrefix": strings.HasPrefix, + } + + tpl, err := template.New(name).Funcs(funcs).Parse(data) + if err != nil { + klog.ErrorS(err, "Failed to parse template file", "template", name) + return "", fmt.Errorf("failed to parse template %q: %w", name, err) + } + + b := &strings.Builder{} + err = tpl.Execute(b, td) + if err != nil { + klog.ErrorS(err, "Executing template failed", "template", name) + return "", fmt.Errorf("failed to execute template %q: %w", name, err) + } + + result := b.String() + klog.InfoS("Templating successful", "template", name, "postTemplating", result) + + return result, nil +} + +func (td *TemplatingData) String() (string, error) { + b, err := json.MarshalIndent(td, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal templating data to json: %w", err) + } + + return string(b), nil +} + +func (td *TemplatingData) FragmentString() (string, error) { + fragment := make(map[string]interface{}) + fragment["Current"] = td.Current + fragment["Previous"] = td.Previous + fragment["YMinus2"] = td.YMinus2 + fragment["RHOCPMinorY"] = td.RHOCPMinorY + fragment["RHOCPMinorY1"] = td.RHOCPMinorY1 + fragment["RHOCPMinorY2"] = td.RHOCPMinorY2 + + b, err := json.MarshalIndent(fragment, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal fragment of templating data to json: %w", err) + } + + return string(b), nil +} diff --git a/test/pkg/util/events.go b/test/pkg/util/events.go new file mode 100644 index 0000000000..359878ecff --- /dev/null +++ b/test/pkg/util/events.go @@ -0,0 +1,343 @@ +package util + +import ( + "encoding/json" + "os" + "slices" + "sync" + "text/template" + "time" +) + +type EventManager interface { + AddEvent(e IEvent) + WriteToFiles(intervalsFile, timelinesFile string) error + GetJUnit() *JUnitTestSuites +} + +type eventManager struct { + name string + events []IEvent + suites map[string]suite + mu sync.Mutex + start time.Time +} + +type suite struct { + name string + start time.Time + events []IEvent +} + +func NewEventManager(name string) EventManager { + return &eventManager{ + name: name, + suites: make(map[string]suite), + start: time.Now(), + } +} + +func (em *eventManager) AddEvent(e IEvent) { + em.mu.Lock() + defer em.mu.Unlock() + + em.events = append(em.events, e) + + if s, ok := em.suites[e.GetSuite()]; !ok { + em.suites[e.GetSuite()] = suite{ + name: e.GetSuite(), + start: time.Now(), + events: []IEvent{e}, + } + } else { + s.events = append(s.events, e) + em.suites[e.GetSuite()] = s + } +} + +func (em *eventManager) WriteToFiles(intervalsFile, timelinesFile string) error { + ivs := em.GetIntervals() + + contents, err := json.MarshalIndent(ivs, "", " ") + if err != nil { + return err + } + + if err := os.WriteFile(intervalsFile, contents, 0644); err != nil { + return err + } + + data := struct { + Title string + Data string + }{ + Title: "MicroShift Test Harness - Compose phase timelines", + Data: string(contents), + } + + tpl, err := template.New("timelines").Parse(timelines) + if err != nil { + return err + } + f, err := os.Create(timelinesFile) + if err != nil { + return err + } + return tpl.Execute(f, data) +} + +func (em *eventManager) GetIntervals() []Interval { + intervals := []Interval{} + + for _, suite := range em.suites { + for _, event := range suite.events { + intervals = append(intervals, event.GetInterval()) + } + } + + slices.SortFunc(intervals, func(a Interval, b Interval) int { + return int(a.Start.Sub(b.Start).Microseconds()) + }) + + return intervals +} + +func (em *eventManager) GetJUnit() *JUnitTestSuites { + jsuites := &JUnitTestSuites{ + Name: em.name, + Time: int(time.Since(em.start).Seconds()), + Timestamp: em.start, + } + + for suiteName, suite := range em.suites { + jsuite := JUnitTestSuite{ + Name: suiteName, + Time: int(time.Since(suite.start).Seconds()), + Timestamp: suite.start, + } + + for _, event := range suite.events { + jtest := event.GetJUnitTestCase() + jsuite.TestCases = append(jsuite.TestCases, jtest) + jsuite.Tests += 1 + if jtest.Failure != nil { + jsuite.Failures += 1 + } else if jtest.Skipped != nil { + jsuite.Skipped += 1 + } + } + + jsuites.TestSuites = append(jsuites.TestSuites, jsuite) + } + + return jsuites +} + +type IEvent interface { + GetJUnitTestCase() JUnitTestCase + GetSuite() string + GetClass() string + GetInterval() Interval + GetName() string +} + +var _ IEvent = (*Event)(nil) + +type Event struct { + Name string + Suite string + ClassName string + Start time.Time + End time.Time + SystemOut string +} + +func (e *Event) GetName() string { + return e.Name +} + +func (e *Event) GetInterval() Interval { + return Interval{ + Name: e.Name, + Suite: e.Suite, + ClassName: e.ClassName, + Start: e.Start, + End: e.End, + Result: "ok", + } +} + +func (e *Event) GetSuite() string { + return e.Suite +} + +func (e *Event) GetClass() string { + return e.ClassName +} + +func (e *Event) GetJUnitTestCase() JUnitTestCase { + return JUnitTestCase{ + Name: e.Name, + ClassName: e.ClassName, + Time: int(e.End.Sub(e.Start).Seconds()), + SystemOut: e.SystemOut, + } +} + +var _ IEvent = (*SkippedEvent)(nil) + +type SkippedEvent struct { + Event + Message string +} + +func (e *SkippedEvent) GetJUnitTestCase() JUnitTestCase { + tc := e.Event.GetJUnitTestCase() + tc.Skipped = &JUnitSkipped{ + Message: e.Message, + } + return tc +} + +func (e *SkippedEvent) GetInterval() Interval { + i := e.Event.GetInterval() + i.Result = "skipped" + return i +} + +var _ IEvent = (*FailedEvent)(nil) + +type FailedEvent struct { + Event + Message string + Content string +} + +func (e *FailedEvent) GetJUnitTestCase() JUnitTestCase { + tc := e.Event.GetJUnitTestCase() + tc.Failure = &JUnitFailure{ + Message: e.Message, + Content: e.Content, + } + return tc +} + +func (e *FailedEvent) GetInterval() Interval { + i := e.Event.GetInterval() + i.Result = "failed" + return i +} + +type Interval struct { + Name string + Suite string + ClassName string + Start time.Time + End time.Time + Result string +} + +// adapted from https://github.com/openshift/origin/tree/master/e2echart +const timelines = ` + + + + + + {{ .Title }} + + + + + + + + + + + + + +
+ + + +` diff --git a/test/pkg/util/events_test.go b/test/pkg/util/events_test.go new file mode 100644 index 0000000000..48f4a31a91 --- /dev/null +++ b/test/pkg/util/events_test.go @@ -0,0 +1,75 @@ +package util + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_Events(t *testing.T) { + em := NewEventManager("compose") + em.AddEvent(&Event{ + Name: "rhel-9.2", + Suite: "compose", + ClassName: "commit", + Start: time.Now().Add(-100 * time.Second), + End: time.Now(), + SystemOut: "test output", + }) + em.AddEvent(&FailedEvent{ + Event: Event{ + Name: "centos9", + Suite: "compose", + ClassName: "image-download", + Start: time.Now().Add(-10 * time.Second), + End: time.Now(), + SystemOut: "test output", + }, + Message: "something wrong", + Content: "very wrong", + }) + + em.AddEvent(&SkippedEvent{ + Event: Event{ + Name: "rhel-9.3", + Suite: "compose", + ClassName: "commit", + Start: time.Now().Add(-10 * time.Second), + End: time.Now(), + SystemOut: "test output", + }, + Message: "everything is fine, as expected, commit already on disk", + }) + time.Sleep(1 * time.Second) + fmt.Printf("%v\n", em) + junit := em.GetJUnit() + assert.NotNil(t, junit) + + assert.Equal(t, "compose", junit.Name) + assert.Equal(t, 1, junit.Time) + assert.Len(t, junit.TestSuites, 1) + + suite0 := junit.TestSuites[0] + assert.Equal(t, "compose", suite0.Name) + assert.Equal(t, 1, suite0.Time) + assert.Equal(t, 3, suite0.Tests) + assert.Equal(t, 1, suite0.Failures) + assert.Equal(t, 1, suite0.Skipped) + + test0 := suite0.TestCases[0] + assert.Equal(t, "rhel-9.2", test0.Name) + assert.Equal(t, "commit", test0.ClassName) + + test1 := suite0.TestCases[1] + assert.Equal(t, "centos9", test1.Name) + assert.Equal(t, "image-download", test1.ClassName) + + test2 := suite0.TestCases[2] + assert.Equal(t, "rhel-9.3", test2.Name) + assert.Equal(t, "commit", test2.ClassName) + + _, err := junit.Marshal() + assert.NoError(t, err) +} diff --git a/test/pkg/util/junit.go b/test/pkg/util/junit.go new file mode 100644 index 0000000000..c97bdcb8cc --- /dev/null +++ b/test/pkg/util/junit.go @@ -0,0 +1,70 @@ +package util + +import ( + "encoding/xml" + "fmt" + "os" + "time" +) + +type JUnitTestSuites struct { + XMLName xml.Name `xml:"testsuites"` + Name string `xml:"name,attr,omitempty"` + Time int `xml:"time,attr"` + Timestamp time.Time `xml:"timestamp,attr,omitempty"` + TestSuites []JUnitTestSuite +} + +func (j *JUnitTestSuites) WriteToFile(path string) error { + contents, err := j.Marshal() + if err != nil { + return fmt.Errorf("failed to marshal junit: %w", err) + } + err = os.WriteFile(path, contents, 0644) + if err != nil { + return fmt.Errorf("failed to write junit to file %q: %w", path, err) + } + return nil +} + +// Prow's junit lens does not support nested TestSuites +// https://github.com/k8s-ci-robot/test-infra/blob/d7867ec05b41/prow/spyglass/lenses/junit/lens.go#L107 + +type JUnitTestSuite struct { + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr,omitempty"` + Time int `xml:"time,attr"` + Timestamp time.Time `xml:"timestamp,attr,omitempty"` + Tests int `xml:"tests,attr,omitempty"` + Failures int `xml:"failures,attr,omitempty"` + Skipped int `xml:"skipped,attr,omitempty"` + TestCases []JUnitTestCase +} + +type JUnitTestCase struct { + XMLName xml.Name `xml:"testcase"` + Name string `xml:"name,attr,omitempty"` + ClassName string `xml:"classname,attr,omitempty"` + Time int `xml:"time,attr,omitempty"` + SystemOut string `xml:"system-out,omitempty"` + Skipped *JUnitSkipped `xml:"skipped,omitempty"` + Failure *JUnitFailure `xml:"failure,omitempty"` +} + +type JUnitSkipped struct { + Message string `xml:"message,attr"` +} + +type JUnitFailure struct { + Message string `xml:"message,attr"` + Content string `xml:",chardata"` +} + +func (suites *JUnitTestSuites) Marshal() ([]byte, error) { + suites.Time = int(time.Since(suites.Timestamp).Seconds()) + bs, err := xml.MarshalIndent(suites, "", " ") + if err != nil { + return nil, err + } + return bs, nil +} diff --git a/test/pkg/util/mocks/composer.go b/test/pkg/util/mocks/composer.go new file mode 100644 index 0000000000..7fa039464c --- /dev/null +++ b/test/pkg/util/mocks/composer.go @@ -0,0 +1,27 @@ +package mocks + +import ( + "github.com/openshift/microshift/test/pkg/compose/helpers" + "github.com/stretchr/testify/mock" +) + +var _ helpers.Composer = (*ComposerMock)(nil) + +type ComposerMock struct { + mock.Mock +} + +func (c *ComposerMock) AddSource(toml string) error { + args := c.Called(toml) + return args.Error(0) +} + +func (c *ComposerMock) DeleteSource(id string) error { + args := c.Called(id) + return args.Error(0) +} + +func (c *ComposerMock) ListSources() ([]string, error) { + args := c.Called() + return args.Get(0).([]string), args.Error(1) +} diff --git a/test/pkg/util/mocks/events.go b/test/pkg/util/mocks/events.go new file mode 100644 index 0000000000..27078803a3 --- /dev/null +++ b/test/pkg/util/mocks/events.go @@ -0,0 +1,24 @@ +package mocks + +import ( + "github.com/openshift/microshift/test/pkg/util" + "github.com/stretchr/testify/mock" +) + +var _ util.EventManager = (*EventManagerMock)(nil) + +type EventManagerMock struct { + mock.Mock +} + +func (em *EventManagerMock) AddEvent(e util.IEvent) { + em.Called(e) +} + +func (em *EventManagerMock) WriteToFiles(intervalsFile string, timelinesFile string) error { + panic("unimplemented") +} + +func (em *EventManagerMock) GetJUnit() *util.JUnitTestSuites { + panic("unimplemented") +} diff --git a/test/pkg/util/paths.go b/test/pkg/util/paths.go new file mode 100644 index 0000000000..005a254f38 --- /dev/null +++ b/test/pkg/util/paths.go @@ -0,0 +1,65 @@ +package util + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +type Paths struct { + MicroShiftRepoRootPath string + TestDirPath string + ImageBlueprintsPath string + ArtifactsMainDir string + BuildLogsDir string + BuildsDir string + OSTreeRepoDir string + VMStorageDir string + BootCImages string + RPMRepos string +} + +func NewPaths() (*Paths, error) { + wd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get working dir: %w", err) + } + return newPaths(wd, os.MkdirAll) +} + +func newPaths(testDirPath string, mkdirAll func(string, os.FileMode) error) (*Paths, error) { + microShiftRepoRootPath := filepath.Join(testDirPath, "..") + artifactsMainDir := filepath.Join(microShiftRepoRootPath, "_output", "test-images") + + paths := &Paths{ + MicroShiftRepoRootPath: microShiftRepoRootPath, + TestDirPath: testDirPath, + ImageBlueprintsPath: filepath.Join(testDirPath, "image-blueprints"), + ArtifactsMainDir: artifactsMainDir, + BuildLogsDir: filepath.Join(artifactsMainDir, "build-logs"), + BuildsDir: filepath.Join(artifactsMainDir, "builds"), + OSTreeRepoDir: filepath.Join(artifactsMainDir, "repo"), + VMStorageDir: filepath.Join(artifactsMainDir, "vm-storage"), + BootCImages: filepath.Join(artifactsMainDir, "bootc-images"), + RPMRepos: filepath.Join(artifactsMainDir, "rpm-repos"), + } + + toCreate := []string{ + paths.ArtifactsMainDir, + paths.BuildLogsDir, + paths.BuildsDir, + paths.OSTreeRepoDir, + paths.VMStorageDir, + paths.BootCImages, + paths.RPMRepos, + } + errs := []error{} + for _, p := range toCreate { + if err := mkdirAll(p, 0755); err != nil { + errs = append(errs, err) + } + } + + return paths, errors.Join(errs...) +} diff --git a/test/pkg/util/paths_test.go b/test/pkg/util/paths_test.go new file mode 100644 index 0000000000..be806c8a8b --- /dev/null +++ b/test/pkg/util/paths_test.go @@ -0,0 +1,42 @@ +package util + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewPaths(t *testing.T) { + createdDirs := []string{} + + mkdirAll := func(p string, _ os.FileMode) error { + createdDirs = append(createdDirs, p) + return nil + } + + testDir := "/home/user/microshift/test" + + paths, err := newPaths(testDir, mkdirAll) + assert.NoError(t, err) + assert.NotNil(t, paths) + + assert.Equal(t, "/home/user/microshift", paths.MicroShiftRepoRootPath) + assert.Equal(t, "/home/user/microshift/test", paths.TestDirPath) + assert.Equal(t, "/home/user/microshift/test/image-blueprints", paths.ImageBlueprintsPath) + assert.Equal(t, "/home/user/microshift/_output/test-images", paths.ArtifactsMainDir) + assert.Equal(t, "/home/user/microshift/_output/test-images/build-logs", paths.BuildLogsDir) + assert.Equal(t, "/home/user/microshift/_output/test-images/builds", paths.BuildsDir) + assert.Equal(t, "/home/user/microshift/_output/test-images/repo", paths.OSTreeRepoDir) + assert.Equal(t, "/home/user/microshift/_output/test-images/vm-storage", paths.VMStorageDir) + assert.Equal(t, "/home/user/microshift/_output/test-images/bootc-images", paths.BootCImages) + assert.Equal(t, "/home/user/microshift/_output/test-images/rpm-repos", paths.RPMRepos) + + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/build-logs") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/builds") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/repo") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/vm-storage") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/bootc-images") + assert.Contains(t, createdDirs, "/home/user/microshift/_output/test-images/rpm-repos", paths.RPMRepos) +} diff --git a/test/pkg/util/run_command.go b/test/pkg/util/run_command.go new file mode 100644 index 0000000000..6b41a3ddce --- /dev/null +++ b/test/pkg/util/run_command.go @@ -0,0 +1,41 @@ +package util + +import ( + "bytes" + "context" + "os/exec" + "regexp" + "strings" + "time" + + "k8s.io/klog/v2" +) + +func RunCommand(c ...string) (string, string, error) { + return RunCommandWithContext(context.Background(), c...) +} + +func RunCommandWithContext(ctx context.Context, c ...string) (string, string, error) { + cmd := exec.CommandContext(ctx, c[0], c[1:]...) + + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb + + klog.InfoS("Running command", "cmd", cmd) + start := time.Now() + err := cmd.Run() + dur := time.Since(start) + + out := strings.Trim(outb.String(), "\n") + serr := errb.String() + klog.InfoS("Command complete", "duration", dur, "cmd", cmd, "stdout", redactOutput(out), "stderr", redactOutput(serr), "err", err) + + return out, serr, err +} + +// redactOutput overwrites sensitive data when logging command outputs +func redactOutput(output string) string { + rx := regexp.MustCompile("gpgkeys.*") + return rx.ReplaceAllString(output, "gpgkeys = REDACTED") +} diff --git a/test/pkg/util/toml.go b/test/pkg/util/toml.go new file mode 100644 index 0000000000..6cc0617da0 --- /dev/null +++ b/test/pkg/util/toml.go @@ -0,0 +1,29 @@ +package util + +import ( + "fmt" + "strings" +) + +// GetTOMLFieldValue obtains value of first variable named `field`. +// It does not use the TOML parser as it is expected to be used on +// TOML files that are yet to be templated. +// +// Primary goal is to stop relying on assumption that filename without extension is the name/id of Blueprint/Source. +// Because Blueprints/Sources are removed prior re-adding, the name/id must be precise. +func GetTOMLFieldValue(data, field string) (string, error) { + lines := strings.Split(data, "\n") + for _, line := range lines { + if strings.HasPrefix(line, field) { + fields := strings.Split(line, "\"") + if len(fields) != 3 { + return "", fmt.Errorf("found matching field in TOML but splitting with double quotes gave unexpected results, line=%q, after split=%q", line, fields) + } + // name = "VALUE" + // ------- ----- - + // 0 1 2 + return fields[1], nil + } + } + return "", nil +}