├── internal ├── regionsrv │ ├── fixtures │ │ ├── valid.pem │ │ ├── valid.sha256 │ │ └── hosts │ ├── hostsfile.go │ ├── ca.go │ ├── zypper.go │ ├── server.go │ ├── hostsfile_test.go │ ├── ca_test.go │ ├── zypper_test.go │ └── server_test.go ├── testdata │ ├── bad.xml │ ├── empty-subscriptions.json │ ├── credentials.txt │ ├── suseconnect.txt │ ├── subscriptions.json │ ├── products-sle12.json │ └── installed.xml ├── error_test.go ├── version_test.go ├── suseconnect.go ├── error.go ├── version.go ├── setup_test.go ├── credentials.go ├── installed_product.go ├── logger.go ├── installed_product_test.go ├── suseconnect_test.go ├── credentials_test.go ├── subscriptions.go ├── configuration.go ├── configuration_test.go ├── logger_test.go ├── service.go ├── subscriptions_test.go ├── products.go ├── products_test.go ├── result.txt └── service_test.go ├── .gitignore ├── go.mod ├── .github ├── workflows │ └── ci.yaml └── dependabot.yml ├── go.sum ├── Makefile ├── Changelog ├── cmd └── container-suseconnect │ └── main.go ├── LICENSE └── README.md /internal/regionsrv/fixtures/valid.pem: -------------------------------------------------------------------------------- 1 | valid -------------------------------------------------------------------------------- /internal/testdata/bad.xml: -------------------------------------------------------------------------------- 1 | >bad 2 | 3 | -------------------------------------------------------------------------------- /internal/testdata/empty-subscriptions.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | build/container-suseconnect 3 | Vagrantfile 4 | .vagrant 5 | -------------------------------------------------------------------------------- /internal/regionsrv/fixtures/valid.sha256: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSE/container-suseconnect/HEAD/internal/regionsrv/fixtures/valid.sha256 -------------------------------------------------------------------------------- /internal/regionsrv/fixtures/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost 2 | ::1 localhost ip6-localhost ip6-loopback 3 | fe00::0 ip6-localnet 4 | ff00::0 ip6-mcastprefix 5 | ff02::1 ip6-allnodes 6 | ff02::2 ip6-allrouters 7 | 172.17.0.2 6e4bb7cb2cf7 8 | -------------------------------------------------------------------------------- /internal/testdata/credentials.txt: -------------------------------------------------------------------------------- 1 | username= SCC_a6994b1d3ae14b35agc7cef46b4fff9a 2 | # Some comment 3 | password =10yb1x6bd159g741ad420fd5aa5083e4 4 | # system token for better instance mapping 5 | system_token=36531d07-a283-441b-a02a-1cd9a88b0d5d 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SUSE/container-suseconnect 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/mssola/capture v1.1.0 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /internal/testdata/suseconnect.txt: -------------------------------------------------------------------------------- 1 | ---- 2 | ## SUSEConnect configuration file example 3 | 4 | ## URL of the registration server. (default: https://scc.suse.com) 5 | # url: https://scc.suse.com 6 | url: https://smt.test.lan 7 | 8 | ## Registration code to use for the base product on the system 9 | # regcode: 10 | 11 | ## Language code to use for error messages (default: $LANG) 12 | # language: 13 | 14 | ## Do not verify SSL certificates when using https (default: false) 15 | insecure: true 16 | 17 | -------------------------------------------------------------------------------- /internal/error_test.go: -------------------------------------------------------------------------------- 1 | package containersuseconnect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsCredentialsNotFoundError(t *testing.T) { 10 | err := &SuseConnectError{ 11 | ErrorCode: CredentialsNotFoundError, 12 | } 13 | 14 | assert.True(t, IsCredentialsNotFoundError(err)) 15 | } 16 | 17 | func TestIsNotCredentialsNotFoundError(t *testing.T) { 18 | err := &SuseConnectError{ 19 | ErrorCode: InvalidCredentialsError, 20 | } 21 | 22 | assert.False(t, IsCredentialsNotFoundError(err)) 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Golang Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | env: 10 | GOFIPS140: v1.0.0 11 | steps: 12 | - uses: actions/checkout@v6 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v6 16 | with: 17 | go-version: 'stable' 18 | 19 | - name: Install Staticcheck 20 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 21 | 22 | - name: Build 23 | run: go build -v ./... 24 | 25 | - name: make tests 26 | run: make test 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # To get started with Dependabot version updates, you'll need to specify which 3 | # package ecosystems to update and where the package manifests are located. 4 | # Please see the documentation for all configuration options: 5 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "gomod" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mssola/capture v1.1.0 h1:6eQlJabrKukAgp4+ytyEB+AACciMqA30H2WRhhHC0xk= 4 | github.com/mssola/capture v1.1.0/go.mod h1:pHi3j6hBxMY+w6folEomycA/3aUz/4VtXRlKJaBcY7E= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 8 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /internal/testdata/subscriptions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 112, 4 | "regcode": "35098ff7", 5 | "name": "Subscription 2", 6 | "type": null, 7 | "status": "NOTACTIVATED", 8 | "starts_at": "2014-05-14T09:13:26.589Z", 9 | "expires_at": null, 10 | "system_limit": 15, 11 | "systems_count": 1, 12 | "virtual_count": null, 13 | "product_classes": ["7260"], 14 | "families": ["sles", "sled"], 15 | "systems": [{ "id": 117, "login": "login2", "password": "password2" }], 16 | "product_ids": [239, 238, 240] 17 | }, 18 | { 19 | "id": 113, 20 | "regcode": "35098ff7-expired", 21 | "name": "Subscription 3", 22 | "type": null, 23 | "status": "EXPIRED", 24 | "starts_at": "2010-05-14T09:13:26.589Z", 25 | "expires_at": "2014-05-14T09:13:26.589Z", 26 | "system_limit": 1, 27 | "systems_count": 1, 28 | "virtual_count": null, 29 | "product_classes": ["7260"], 30 | "families": ["sles", "sled"], 31 | "systems": [{ "id": 117, "login": "login2", "password": "password2" }], 32 | "product_ids": [239, 238, 240] 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_VERBOSE := -v 2 | CS_BUILD_DIR := $(PWD)/build/container-suseconnect 3 | PROJECT := github.com/SUSE/container-suseconnect 4 | 5 | export GO111MODULE=auto 6 | 7 | ifneq "$(VERBOSE)" "1" 8 | GO_VERBOSE= 9 | .SILENT: 10 | endif 11 | 12 | all: 13 | rm -rf $(CS_BUILD_DIR) 14 | mkdir -p $(CS_BUILD_DIR) 15 | GOBIN=$(CS_BUILD_DIR) go install -ldflags='-w -s' -a $(GO_VERBOSE) ./... 16 | 17 | .PHONY: test 18 | test: test-unit validate-go 19 | 20 | .PHONY: test-unit 21 | test-unit: 22 | go test $(GO_VERBOSE) ./... 23 | 24 | .PHONY: validate-go 25 | validate-go: 26 | build/ci/climate -t 80 -o internal 27 | build/ci/climate -t 80 -o internal/regionsrv 28 | go mod verify 29 | 30 | @which gofmt >/dev/null 2>/dev/null || (echo "ERROR: gofmt not found." && false) 31 | test -z "$$(gofmt -s -l . | tee /dev/stderr)" 32 | @which staticcheck >/dev/null 2>/dev/null || echo "WARNING: staticcheck not found." || true 33 | @which "staticcheck" >/dev/null 2>/dev/null && "$$(staticcheck -tests=false 2>&1 | tee /dev/stderr)" || true 34 | @go doc cmd/vet >/dev/null 2>/dev/null || (echo "ERROR: go vet not found." && false) 35 | test -z "$$(go vet $$(go list $(PROJECT)/... ) 2>&1 | tee /dev/stderr)" 36 | -------------------------------------------------------------------------------- /internal/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package containersuseconnect 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestGetVersionWithVersionRevisionUnset(t *testing.T) { 23 | version = "" 24 | revision = "" 25 | 26 | assert.Equal(t, "devel", GetVersion()) 27 | } 28 | 29 | func TestGetVersionWithVersionSet(t *testing.T) { 30 | version = "1.2.3" 31 | revision = "" 32 | 33 | assert.Equal(t, "1.2.3", GetVersion()) 34 | } 35 | 36 | func TestGetVersionWithRevisionSet(t *testing.T) { 37 | version = "" 38 | revision = "gh12345" 39 | 40 | assert.Equal(t, "devel (gh12345)", GetVersion()) 41 | } 42 | 43 | func TestGetVersionWithVersionRevisionSet(t *testing.T) { 44 | version = "1.2.3" 45 | revision = "gh12345" 46 | 47 | assert.Equal(t, "1.2.3 (gh12345)", GetVersion()) 48 | } 49 | -------------------------------------------------------------------------------- /internal/suseconnect.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "log" 19 | ) 20 | 21 | const ( 22 | sccURLStr = "https://scc.suse.com" 23 | ) 24 | 25 | // SUSEConnectData has all the relevant data from SUSEConnect. 26 | type SUSEConnectData struct { 27 | SccURL string 28 | Insecure bool 29 | } 30 | 31 | func (data *SUSEConnectData) separator() byte { 32 | return ':' 33 | } 34 | 35 | func (data *SUSEConnectData) locations() []string { 36 | return []string{"/etc/SUSEConnect", "/run/secrets/SUSEConnect"} 37 | } 38 | 39 | func (data *SUSEConnectData) onLocationsNotFound() bool { 40 | data.SccURL = sccURLStr 41 | return true 42 | } 43 | 44 | func (data *SUSEConnectData) setValues(key, value string) { 45 | switch key { 46 | case "url": 47 | data.SccURL = value 48 | case "insecure": 49 | data.Insecure = value == "true" 50 | default: 51 | log.Printf("Warning: Unknown key '%v'", key) 52 | } 53 | } 54 | 55 | func (data *SUSEConnectData) afterParseCheck() error { 56 | if data.SccURL == "" { 57 | data.SccURL = sccURLStr 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | Wed Jun 67 13:08:02 CET 2020 Ralf Haferkamp 2 | 3 | * Update to version 2.3.0: 4 | - Introduce "containerbuild-regionsrv" support and add 5 | "susecloud" zypper url-resolver plugin to allow building SLE 6 | containers on public cloud on-demand instances 7 | - Fix usage with RMT when repositories require authentication 8 | 9 | Wed Nov 27 16:53:32 CET 2019 Ralf Haferkamp 10 | 11 | * Update to version 2.2.0: 12 | - Update go version and dependencies 13 | - Fix usage with RMT and SMT 14 | - parse the /etc/products.d/*.prod files 15 | 16 | Wed Jun 19 14:32:06 CET 2019 Sascha Grunert 17 | 18 | * Update to version 2.1.0: 19 | - Fix function comments based on best practices from Effective Go 20 | - Interacting with SCC behind proxy and SMT 21 | 22 | Mon Dec 10 10:44:06 CET 2018 Sascha Grunert 23 | 24 | * Update to version 2.0.0: 25 | - Add command line interface 26 | - Add `ADDITIONAL_MODULES` capability to enable further extension 27 | modules during image build and run 28 | - Add documentation about how to build docker images on non SLE 29 | distributions 30 | - Improve documentation to clarify how container-suseconnect works in 31 | a Dockerfile 32 | - Improve error handling on non SLE hosts 33 | - Fix bug which makes container-suseconnect work on SLE15 based 34 | distributions 35 | 36 | Wed Nov 11 15:21:05 CET 2015 Flavio Castelli 37 | 38 | * Update to version 1.1.0: 39 | - Improve logging 40 | 41 | Thu Jun 18 18:45:28 CET 2015 Flavio Castelli 42 | 43 | * Update to version 1.0.1: 44 | - Use default values if SUSEConnectData is not found 45 | 46 | Tue Jun 16 17:22:47 CET 2015 Flavio Castelli 47 | 48 | * Initial release: 1.0.0 49 | -------------------------------------------------------------------------------- /internal/regionsrv/hostsfile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "os" 21 | "strings" 22 | ) 23 | 24 | var hostsFile = "/etc/hosts" 25 | 26 | // UpdateHostsFile updates the hosts file with the given hostname and IP. 27 | func UpdateHostsFile(hostname string, ip string) error { 28 | content, err := os.ReadFile(hostsFile) 29 | if err != nil { 30 | return fmt.Errorf("can't read %s file: %v", hostsFile, err.Error()) 31 | } 32 | 33 | lines := strings.Split(string(content), "\n") 34 | newcontent := "" 35 | hostChecked := false 36 | shorthost := strings.Split(hostname, ".")[0] 37 | 38 | for _, line := range lines { 39 | fields := strings.Fields(line) 40 | if len(fields) >= 2 && fields[1] == hostname { 41 | if fields[0] != ip { 42 | log.Printf("updating hosts entry for %s", hostname) 43 | line = fmt.Sprintf("%s %s %s\n", ip, hostname, shorthost) 44 | } 45 | 46 | hostChecked = true 47 | } 48 | 49 | newcontent += line + "\n" 50 | } 51 | 52 | if !hostChecked { 53 | newcontent += fmt.Sprintf("%s %s %s\n", ip, hostname, shorthost) 54 | } 55 | 56 | err = os.WriteFile(hostsFile, []byte(newcontent), 0o644) 57 | if err != nil { 58 | return fmt.Errorf("can't write %s file: %v", hostsFile, err.Error()) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import "errors" 18 | 19 | const ( 20 | // CredentialsNotFoundError means missing credentials 21 | CredentialsNotFoundError = iota 22 | // InvalidCredentialsError indicates an error parsing credentials 23 | InvalidCredentialsError 24 | // NetworkError is a placeholder for generic network communication 25 | // errors 26 | NetworkError 27 | // InstalledProductError signals issues with the installed products 28 | InstalledProductError 29 | // SubscriptionServerError means that the subscription server did 30 | // something unexpected 31 | SubscriptionServerError 32 | // SubscriptionError marks issues with the actual subscription 33 | SubscriptionError 34 | // RepositoryError indicates that there is something wrong with the 35 | // repository that the subscription server gave us 36 | RepositoryError 37 | ) 38 | 39 | // SuseConnectError is a custom error type allowing us to distinguish between 40 | // different error kinds via the `ErrorCode` field 41 | type SuseConnectError struct { 42 | ErrorCode int 43 | message string 44 | } 45 | 46 | func (s *SuseConnectError) Error() string { 47 | return s.message 48 | } 49 | 50 | func IsCredentialsNotFoundError(err error) bool { 51 | var scerr *SuseConnectError 52 | 53 | if errors.As(err, &scerr) { 54 | return scerr.ErrorCode == CredentialsNotFoundError 55 | } 56 | 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "fmt" 19 | "runtime/debug" 20 | "strconv" 21 | ) 22 | 23 | var ( 24 | version string 25 | revision string 26 | ) 27 | 28 | // GetVersion returns the version and revision if available. 29 | // If both version and revision are found, it returns "%version (%revision)". 30 | // If only version is found, it returns "%version". 31 | // If no version is found but a revision is, it returns "devel (%revision)". 32 | // If no version and no revision are found, it returns "devel". 33 | // The revision can taken from the binary if it was stamped with version 34 | // control information. 35 | func GetVersion() string { 36 | if version == "" { 37 | version = "devel" 38 | } 39 | 40 | if revision == "" { 41 | bi, ok := debug.ReadBuildInfo() 42 | 43 | if !ok { 44 | return version 45 | } 46 | 47 | var vcsRevision string 48 | var vcsModified bool 49 | 50 | for _, s := range bi.Settings { 51 | switch s.Key { 52 | case "vcs.revision": 53 | vcsRevision = s.Value 54 | case "vcs.modified": 55 | vcsModified, _ = strconv.ParseBool(s.Value) 56 | } 57 | } 58 | 59 | if vcsRevision == "" { 60 | return version 61 | } else { 62 | revision = vcsRevision[:7] 63 | 64 | if vcsModified { 65 | revision = revision + "+modified" 66 | } 67 | } 68 | } 69 | 70 | return fmt.Sprintf("%s (%s)", version, revision) 71 | } 72 | -------------------------------------------------------------------------------- /internal/setup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "log" 21 | "os" 22 | "regexp" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | // Handy functions to be used by the test suite. 28 | 29 | // Private global value for the tests. It stores all the contents that have 30 | // been logged after a `prepareLogger` call. 31 | var logged *bytes.Buffer 32 | 33 | // It initializes the logger infrastructure for tests. 34 | func prepareLogger() { 35 | logged = bytes.NewBuffer([]byte{}) 36 | log.SetOutput(logged) 37 | } 38 | 39 | // Make sure that the logged string matches the given expected string. 40 | func shouldHaveLogged(t *testing.T, str string) { 41 | original := logged.String() 42 | if strings.TrimSpace(original) == "" { 43 | t.Fatal("Nothing has been logged.\n") 44 | } 45 | 46 | // The logged string includes the timestamp, get rid of it. 47 | re := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\s`) 48 | logStr := strings.TrimSpace(re.ReplaceAllString(original, "")) 49 | 50 | if strings.TrimSpace(str) != logStr { 51 | t.Fatalf("Should have logged: %v, not %v\n", str, logStr) 52 | } 53 | } 54 | 55 | // Capture what is written to Stderr. 56 | func captureStderr(t *testing.T, fn func()) (string, error) { 57 | // redirect Stderr to capture the log written 58 | orig := os.Stderr 59 | r, w, err := os.Pipe() 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | os.Stderr = w 65 | 66 | fn() 67 | 68 | // restore Stderr 69 | os.Stderr = orig 70 | w.Close() 71 | 72 | data, err := io.ReadAll(r) 73 | if err != nil { 74 | return "", err 75 | } 76 | 77 | return string(data), nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/testdata/products-sle12.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "arch": "x86_64", 4 | "product_class": "7261", 5 | "extensions": [ 6 | ], 7 | "product_type": "base", 8 | "identifier": "SLES", 9 | "free": true, 10 | "id": 100874, 11 | "friendly_name": "SUSE Linux Enterprise Server 12 x86_64", 12 | "available": true, 13 | "version": "12", 14 | "name": "SUSE Linux Enterprise Server 12 x86_64", 15 | "description": "SUSE Linux Enterprise offers a comprehensive suite of products built on a single code base. The platform addresses business needs from the smallest thin-client devices to the world's most powerful high-performance computing and mainframe servers. SUSE Linux Enterprise offers common management tools and technology certifications across the platform, and each product is enterprise-class.", 16 | "repositories": [ 17 | { 18 | "distro_target": "sle-12-x86_64", 19 | "name": "SLES12-Updates", 20 | "description": "SLES12-Updates for sle-12-x86_64", 21 | "autorefresh": true, 22 | "url": "https://smt.test.lan/repo/SUSE/Updates/SLE-SERVER/12/x86_64/update", 23 | "id": "941", 24 | "enabled": true 25 | }, 26 | { 27 | "distro_target": "sle-12-x86_64", 28 | "name": "SLES12-Debuginfo-Updates", 29 | "description": "SLES12-Debuginfo-Updates for sle-12-x86_64", 30 | "autorefresh": true, 31 | "url": "https://smt.test.lan/repo/SUSE/Updates/SLE-SERVER/12/x86_64/update_debug", 32 | "id": "942", 33 | "enabled": false 34 | }, 35 | { 36 | "distro_target": "sle-12-x86_64", 37 | "name": "SLES12-Pool", 38 | "description": "SLES12-Pool for sle-12-x86_64", 39 | "autorefresh": false, 40 | "url": "https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product", 41 | "id": "943", 42 | "enabled": true 43 | }, 44 | { 45 | "distro_target": "sle-12-x86_64", 46 | "name": "SLES12-Debuginfo-Pool", 47 | "description": "SLES12-Debuginfo-Pool for sle-12-x86_64", 48 | "autorefresh": false, 49 | "url": "https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product_debug", 50 | "id": "944", 51 | "enabled": false 52 | } 53 | ], 54 | "release_type": null, 55 | "former_identifier": "SUSE_SLES", 56 | "eula_url": "https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product.license/", 57 | "cpe": "cpe:/o:suse:sles:12.0" 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /internal/credentials.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "log" 19 | "os" 20 | ) 21 | 22 | // Credentials holds the host credentials 23 | // NOTE (@mssola): in SCC we have introduced the System-Token credential. For 24 | // now this is not affecting the normal operation of this application, but it 25 | // is something to keep in mind for further developments. 26 | type Credentials struct { 27 | Username string 28 | Password string 29 | SystemToken string 30 | InstanceData string 31 | } 32 | 33 | func (cr *Credentials) separator() byte { 34 | return '=' 35 | } 36 | 37 | func (cr *Credentials) locations() []string { 38 | return []string{ 39 | "/etc/zypp/credentials.d/SCCcredentials", 40 | "/run/secrets/SCCcredentials", 41 | "/run/secrets/credentials.d/SCCcredentials", 42 | } 43 | } 44 | 45 | func (cr *Credentials) onLocationsNotFound() bool { 46 | env_user := os.Getenv("SCC_CREDENTIAL_USERNAME") 47 | env_pass := os.Getenv("SCC_CREDENTIAL_PASSWORD") 48 | env_system_token := os.Getenv("SCC_CREDENTIAL_SYSTEM_TOKEN") 49 | 50 | if env_user != "" && env_pass != "" { 51 | cr.Username = env_user 52 | cr.Password = env_pass 53 | cr.SystemToken = env_system_token 54 | return true 55 | } 56 | 57 | return false 58 | } 59 | 60 | func (cr *Credentials) setValues(key, value string) { 61 | switch key { 62 | case "username": 63 | cr.Username = value 64 | case "password": 65 | cr.Password = value 66 | case "system_token": 67 | cr.SystemToken = value 68 | default: 69 | log.Printf("Warning: Unknown key '%v'", key) 70 | } 71 | } 72 | 73 | func (cr *Credentials) afterParseCheck() error { 74 | if cr.Username == "" { 75 | return loggedError(InvalidCredentialsError, "Can't find username") 76 | } 77 | 78 | if cr.Password == "" { 79 | return loggedError(InvalidCredentialsError, "Can't find password") 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/regionsrv/ca.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "crypto/sha256" 19 | "io" 20 | "os" 21 | "os/exec" 22 | "strings" 23 | ) 24 | 25 | var ( 26 | oldHashFilePath = "/etc/pki/containerbuild-regionsrv.md5" 27 | hashFilePath = "/etc/pki/containerbuild-regionsrv.sha256" 28 | caFilePath = "/etc/pki/trust/anchors/containerbuild-regionsrv.pem" 29 | ) 30 | 31 | // commander is a very simple interface that just implements the `Run` function, 32 | // which returns an error. This interface has merely been introduced to ease up 33 | // testing. 34 | type commander interface { 35 | Run() error 36 | } 37 | 38 | // Returns true if the CA file needs an update, false otherwise. 39 | func updateNeeded(contents string) bool { 40 | if _, err := os.Stat(hashFilePath); os.IsNotExist(err) { 41 | return true 42 | } 43 | 44 | data, err := os.ReadFile(hashFilePath) 45 | if err != nil { 46 | return true 47 | } 48 | 49 | hash := sha256.New() 50 | io.WriteString(hash, contents) 51 | 52 | return strings.TrimSpace(string(data)) != string(hash.Sum(nil)) 53 | } 54 | 55 | // saveCAFile implements `SaveCAFile` by assuming a `commander` type will be 56 | // given. 57 | func saveCAFile(cmd commander, contents string) error { 58 | if !updateNeeded(contents) { 59 | return nil 60 | } 61 | 62 | // Nuke everything before populating things back again. 63 | os.Remove(oldHashFilePath) 64 | os.Remove(hashFilePath) 65 | os.Remove(caFilePath) 66 | 67 | // Save the file 68 | err := os.WriteFile(caFilePath, []byte(contents), 0o644) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // Execute `update-ca-certificates` now. 74 | if err = cmd.Run(); err != nil { 75 | return err 76 | } 77 | 78 | // Save the new checksum 79 | hash := sha256.New() 80 | io.WriteString(hash, contents) 81 | os.WriteFile(hashFilePath, hash.Sum(nil), 0o644) 82 | 83 | return nil 84 | } 85 | 86 | // SaveCAFile creates a certificate file into the right location if it isn't 87 | // already there. This function will call `update-ca-certificates` whenever the 88 | // CA file has been updated. 89 | func SaveCAFile(contents string) error { 90 | cmd := exec.Command("update-ca-certificates") 91 | return saveCAFile(cmd, contents) 92 | } 93 | -------------------------------------------------------------------------------- /internal/testdata/installed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SUSE 4 | SLES 5 | 12 6 | 12 7 | 0 8 | SUSE_SLES 9 | 0 10 | 2024-10-31 11 | x86_64 12 | cpe:/o:suse:sles:12 13 | sles 14 | 15 | sle-12-x86_64 16 | 17 | 18 | 19 | 20 | 21 | 22 | A43242DKD 23 | SUSE Linux Enterprise Server 12 24 | SLES12 25 | SUSE Linux Enterprise offers a comprehensive 26 | suite of products built on a single code base. 27 | The platform addresses business needs from 28 | the smallest thin-client devices to the world's 29 | most powerful high-performance computing 30 | and mainframe servers. SUSE Linux Enterprise 31 | offers common management tools and technology 32 | certifications across the platform, and 33 | each product is enterprise-class. 34 | 35 | cs 36 | da 37 | de 38 | en 39 | en_GB 40 | en_US 41 | es 42 | fi 43 | fr 44 | hu 45 | it 46 | ja 47 | nb 48 | nl 49 | pl 50 | pt 51 | pt_BR 52 | ru 53 | sv 54 | zh 55 | zh_CN 56 | zh_TW 57 | 58 | 59 | https://www.suse.com/releasenotes/x86_64/SUSE-SLES/12/release-notes-sles.rpm 60 | 61 | 62 | SLES 63 | 64 | 65 | en_US 66 | suse 67 | suse/setup/descr 68 | 69 | SUSE_SLE 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /internal/installed_product.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "encoding/xml" 19 | "fmt" 20 | "io" 21 | "os" 22 | ) 23 | 24 | // ProductProvider is used to retrieve the location of the file containing the 25 | // information about the installed product. 26 | type ProductProvider interface { 27 | // Returns the path to the XML file containing the info about the installed 28 | // product. 29 | Location() string 30 | } 31 | 32 | // SUSEProductProvider implements the ProductProvider interface so we can fetch 33 | // the location of the SUSE baseproduct file. 34 | type SUSEProductProvider struct{} 35 | 36 | // Location returns the location of the base product provider. 37 | func (b SUSEProductProvider) Location() string { 38 | return "/etc/products.d/baseproduct" 39 | } 40 | 41 | // InstalledProduct contains all the info that we need from the installed 42 | // product. 43 | type InstalledProduct struct { 44 | Identifier string `xml:"name"` 45 | Version string `xml:"version"` 46 | Arch string `xml:"arch"` 47 | } 48 | 49 | func (p InstalledProduct) String() string { 50 | return fmt.Sprintf("%s-%s-%s", p.Identifier, p.Version, p.Arch) 51 | } 52 | 53 | // Parses installed product data. The passed reader is guaranteed to be 54 | // readable. 55 | func parseInstalledProduct(reader io.Reader) (InstalledProduct, error) { 56 | // We can ignore this error because of the pre-condition of the `reader` 57 | // being actually readable. 58 | xmlData, _ := io.ReadAll(reader) 59 | 60 | var p InstalledProduct 61 | err := xml.Unmarshal(xmlData, &p) 62 | if err != nil { 63 | return InstalledProduct{}, loggedError(InstalledProductError, "Can't parse base product file: %v", err.Error()) 64 | } 65 | 66 | return p, nil 67 | } 68 | 69 | // Read the product file from the standard location 70 | func readInstalledProduct(provider ProductProvider) (InstalledProduct, error) { 71 | if _, err := os.Stat(provider.Location()); os.IsNotExist(err) { 72 | return InstalledProduct{}, loggedError(InstalledProductError, "No base product detected") 73 | } 74 | 75 | xmlFile, err := os.Open(provider.Location()) 76 | if err != nil { 77 | return InstalledProduct{}, loggedError(InstalledProductError, "Can't open base product file: %v", err.Error()) 78 | } 79 | defer xmlFile.Close() 80 | 81 | return parseInstalledProduct(xmlFile) 82 | } 83 | 84 | // GetInstalledProduct gets the installed product on a SUSE machine. 85 | func GetInstalledProduct() (InstalledProduct, error) { 86 | var b SUSEProductProvider 87 | return readInstalledProduct(b) 88 | } 89 | -------------------------------------------------------------------------------- /internal/regionsrv/zypper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "log" 24 | "net/url" 25 | "os" 26 | "strings" 27 | ) 28 | 29 | // ParseStdin parses the standard input as given by zypper and returns a map 30 | // with the parsed parameters. 31 | func ParseStdin() (map[string]string, error) { 32 | params := make(map[string]string) 33 | first := true 34 | 35 | // The zypper plugin protocol is based on STOMP. STOMP messages 36 | // are NUL-terminated. So read the entire message first 37 | reader := bufio.NewReader(os.Stdin) 38 | msg, err := reader.ReadBytes(0) 39 | if err != nil && err != io.EOF { 40 | return nil, err 41 | } 42 | 43 | // Now read the message line by line. URL resolver plugin messages 44 | // are just : header lines and don't contain a body. 45 | sr := bytes.NewReader(msg) 46 | scanner := bufio.NewScanner(sr) 47 | 48 | for scanner.Scan() { 49 | if first { 50 | first = false 51 | } else { 52 | vals := strings.SplitN(scanner.Text(), ":", 2) 53 | if len(vals) == 2 { 54 | params[vals[0]] = vals[1] 55 | } 56 | } 57 | } 58 | 59 | if scanner.Err() != nil { 60 | return nil, scanner.Err() 61 | } 62 | 63 | return params, nil 64 | } 65 | 66 | // PrintResponse prints to standard output with the format expected by zypper. 67 | func PrintResponse(params map[string]string) error { 68 | cfg, err := ReadConfigFromServer() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // Error out if we have no information on the credentials. 74 | if cfg.Username == "" && cfg.Password == "" { 75 | return errors.New("no credentials given") 76 | } 77 | 78 | // Save the contents of the CA file if it doesn't exist already. 79 | if err = SaveCAFile(cfg.Ca); err != nil { 80 | return err 81 | } 82 | 83 | printFromConfiguration(params["path"], cfg) 84 | 85 | return nil 86 | } 87 | 88 | func printFromConfiguration(path string, cfg *ContainerBuildConfig) { 89 | u := url.URL{ 90 | Scheme: "https", 91 | Host: cfg.ServerFqdn, 92 | Path: path, 93 | User: url.UserPassword(cfg.Username, cfg.Password), 94 | } 95 | 96 | log.Print("Received X-Instance-Data") 97 | log.Printf("Resulting URL: %s", u.Redacted()) 98 | 99 | fmt.Printf("RESOLVEDURL\n") 100 | // Add an extra empty line to separate Headers from payload 101 | fmt.Printf("X-Instance-Data:%s\n\n", cfg.InstanceData) 102 | // Message needs to be NUL-terminated 103 | fmt.Printf("%s\000", u.String()) 104 | } 105 | -------------------------------------------------------------------------------- /internal/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "log" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | ) 25 | 26 | // Default path for the log file. 27 | const DefaultLogPath = "/var/log/suseconnect.log" 28 | 29 | // Environment variable used to specify a custom path for the log file. 30 | const LogEnv = "SUSECONNECT_LOG_FILE" 31 | 32 | // getLogEnv returns the value set to the [LogEnv] environment variable. 33 | func getLogEnv() string { 34 | return strings.TrimSpace(os.Getenv(LogEnv)) 35 | } 36 | 37 | // getLogWriter checks if the path can be open and written to 38 | // and returns an [io.WriteCloser] if there are no errors. 39 | func getLogWriter(path string) (io.WriteCloser, error) { 40 | path = strings.TrimSpace(path) 41 | 42 | if len(path) == 0 { 43 | return nil, fmt.Errorf("path is empty") 44 | } 45 | 46 | if !filepath.IsAbs(path) { 47 | return nil, fmt.Errorf("log path is not absolute: %s", path) 48 | } 49 | 50 | lf, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if fi, err := lf.Stat(); err == nil { 56 | if !fi.Mode().IsRegular() { 57 | lf.Close() 58 | return nil, fmt.Errorf("path is not a regular file: %s", path) 59 | } 60 | } 61 | 62 | _, err = fmt.Fprintf(lf, "container-suseconnect %s\n", GetVersion()) 63 | if err != nil { 64 | lf.Close() 65 | return nil, err 66 | } 67 | 68 | return lf, nil 69 | } 70 | 71 | // SetLoggerOutput configures the logger to write to /dev/stderr 72 | // and to a file. 73 | // 74 | // If [LogEnv] is set and writable it writes to the file defined, 75 | // otherwise it writes to [DefaultLogPath]. 76 | func SetLoggerOutput() { 77 | // ensure we are logging to stderr and nowhere else 78 | log.SetOutput(os.Stderr) 79 | 80 | path := getLogEnv() 81 | 82 | if len(path) == 0 { 83 | path = DefaultLogPath 84 | } 85 | 86 | w, err := getLogWriter(path) 87 | 88 | if err == nil { 89 | writer := io.MultiWriter(os.Stderr, w) 90 | log.SetOutput(writer) 91 | log.Printf("Log file location: %s\n", path) 92 | } else { 93 | log.Printf("Failed to set up log file '%s'\n", path) 94 | log.Println(err) 95 | } 96 | } 97 | 98 | // Log the given formatted string with its parameters, and return it 99 | // as a new error. 100 | func loggedError(errorCode int, format string, params ...interface{}) *SuseConnectError { 101 | msg := fmt.Sprintf(format, params...) 102 | log.Print(msg) 103 | return &SuseConnectError{ 104 | ErrorCode: errorCode, 105 | message: msg, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /internal/installed_product_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | ) 21 | 22 | type NotFoundProvider struct{} 23 | 24 | func (m NotFoundProvider) Location() string { 25 | return "testdata/not-found.xml" 26 | } 27 | 28 | func TestFailNonExistantProduct(t *testing.T) { 29 | var b NotFoundProvider 30 | 31 | _, err := readInstalledProduct(b) 32 | if err == nil { 33 | t.Fatal("This file should not exist...") 34 | } 35 | 36 | if err.Error() != "No base product detected" { 37 | t.Fatal("Wrong error message") 38 | } 39 | } 40 | 41 | type NotAllowedProvider struct{} 42 | 43 | func (m NotAllowedProvider) Location() string { 44 | return "/etc/shadow" 45 | } 46 | 47 | func TestFailNotAllowedProduct(t *testing.T) { 48 | var b NotAllowedProvider 49 | 50 | _, err := readInstalledProduct(b) 51 | if err == nil { 52 | t.Fatal("This file should not be available...") 53 | } 54 | 55 | if err.Error() != "Can't open base product file: open /etc/shadow: permission denied" { 56 | t.Fatal("Wrong error message") 57 | } 58 | } 59 | 60 | type BadFormattedProvider struct{} 61 | 62 | func (m BadFormattedProvider) Location() string { 63 | return "testdata/bad.xml" 64 | } 65 | 66 | func TestFailBadFormattedProduct(t *testing.T) { 67 | var b BadFormattedProvider 68 | 69 | _, err := readInstalledProduct(b) 70 | if err == nil { 71 | t.Fatal("This file should have a bad format") 72 | } 73 | 74 | if err.Error() != "Can't parse base product file: EOF" { 75 | t.Fatal("Wrong error message") 76 | } 77 | } 78 | 79 | type MockProvider struct{} 80 | 81 | func (m MockProvider) Location() string { 82 | return "testdata/installed.xml" 83 | } 84 | 85 | func TestMockProvider(t *testing.T) { 86 | var b MockProvider 87 | 88 | p, err := readInstalledProduct(b) 89 | if err != nil { 90 | t.Fatal("It should've read it just fine") 91 | } 92 | 93 | if p.Identifier != "SLES" { 94 | t.Fatal("Wrong product name") 95 | } 96 | 97 | if p.Version != "12" { 98 | t.Fatal("Wrong product version") 99 | } 100 | 101 | if p.Arch != "x86_64" { 102 | t.Fatal("Wrong product arch") 103 | } 104 | 105 | if p.String() != "SLES-12-x86_64" { 106 | t.Fatal("Wrong product string") 107 | } 108 | } 109 | 110 | // This test is useless outside SUSE. Added so the go cover tool is happy. 111 | func TestSUSE(t *testing.T) { 112 | var b SUSEProductProvider 113 | 114 | if _, err := os.Stat(b.Location()); os.IsNotExist(err) { 115 | _, err = GetInstalledProduct() 116 | if err == nil { 117 | t.Fatal("It should fail") 118 | } 119 | 120 | return 121 | } 122 | 123 | _, err := GetInstalledProduct() 124 | if err != nil { 125 | t.Fatal("We assume that is SUSE, so this should be fine") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/regionsrv/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package regionsrv implements all the utilities to interact with on-demand 16 | // Public clouds. 17 | package regionsrv 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "log" 24 | "net" 25 | "os" 26 | ) 27 | 28 | // ContainerBuildConfig contains all the data that is available through the 29 | // containerbuild-regionsrv server running on the host. 30 | type ContainerBuildConfig struct { 31 | InstanceData string `json:"instance-data"` 32 | ServerFqdn string `json:"server-fqdn"` 33 | ServerIP string `json:"server-ip"` 34 | Username string `json:"username"` 35 | Password string `json:"password"` 36 | Ca string `json:"ca"` 37 | } 38 | 39 | // containerBuildSrvAddress returns a string containing the full address of the TCP 40 | // server. You may tweak this by providing `CONTAINER_BUILD_IP` and/or 41 | // `CONTAINER_BUILD_PORT`, otherwise `0.0.0.0` and `7956` are taken as defaults for 42 | // the IP and the port respectively. 43 | func containerBuildSrvAddress() string { 44 | ip := os.Getenv("CONTAINER_BUILD_IP") 45 | if ip == "" { 46 | ip = "0.0.0.0" 47 | } 48 | 49 | port := os.Getenv("CONTAINER_BUILD_PORT") 50 | if port == "" { 51 | port = "7956" 52 | } 53 | 54 | return fmt.Sprintf("%s:%s", ip, port) 55 | } 56 | 57 | // ServerReachable returns true if the containerbuild-regionsrv server is 58 | // reachable, false otherwise. 59 | func ServerReachable() error { 60 | addr, err := net.ResolveTCPAddr("tcp", containerBuildSrvAddress()) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | conn, err := net.DialTCP("tcp", nil, addr) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | conn.Close() 71 | 72 | return nil 73 | } 74 | 75 | // ReadConfigFromServer performs a request against the containerbuild-regionsrv 76 | // server running in the host, and it parses the given response so it can be 77 | // used as a ContainerBuildConfig. 78 | func ReadConfigFromServer() (*ContainerBuildConfig, error) { 79 | addr, err := net.ResolveTCPAddr("tcp", containerBuildSrvAddress()) 80 | log.Printf("Trying to reach suse build server at '%v'", addr.String()) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | conn, err := net.DialTCP("tcp", nil, addr) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | defer conn.Close() 91 | log.Printf("Reading from containerbuild-regionsrv ...") 92 | 93 | d := json.NewDecoder(conn) 94 | 95 | data := &ContainerBuildConfig{} 96 | if err := d.Decode(&data); err != nil { 97 | return nil, err 98 | } 99 | 100 | // If something is really bad on the server side, it may return an empty 101 | // response. Catch this error here. 102 | if data.InstanceData == "" && data.ServerFqdn == "" && data.ServerIP == "" && data.Ca == "" { 103 | return nil, errors.New("empty response from the server") 104 | } 105 | 106 | return data, nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/suseconnect_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package containersuseconnect 17 | 18 | import ( 19 | "bytes" 20 | "log" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestSUSEConnectData(t *testing.T) { 26 | data := &SUSEConnectData{} 27 | 28 | if data.separator() != ':' { 29 | t.Fatal("Wrong separator") 30 | } 31 | 32 | err := data.afterParseCheck() 33 | if err != nil { 34 | t.Fatal("There should not be an error") 35 | } 36 | 37 | if data.SccURL != sccURLStr { 38 | t.Fatal("The URL should be the one from sccURLstr") 39 | } 40 | 41 | locs := data.locations() 42 | 43 | if locs[0] != "/etc/SUSEConnect" { 44 | t.Fatal("Wrong location") 45 | } 46 | 47 | if locs[1] != "/run/secrets/SUSEConnect" { 48 | t.Fatal("Wrong location") 49 | } 50 | 51 | buffer := bytes.NewBuffer([]byte{}) 52 | log.SetOutput(buffer) 53 | data.setValues("unknown", "value") 54 | 55 | // It should log a proper warning. 56 | if !strings.Contains(buffer.String(), "Warning: Unknown key 'unknown'") { 57 | t.Fatal("Wrong warning!") 58 | } 59 | } 60 | 61 | // In the following test we will create a mock that just wraps up the 62 | // `SUSEConnectData` struct and replaces its `location` function for something 63 | // that can be tested. We test for a successful run, since all the possible 64 | // errors have already been tested in the `configuration_test.go` file. 65 | 66 | type SUSEConnectDataMock struct { 67 | data *SUSEConnectData 68 | } 69 | 70 | var locationShouldBeFound = true 71 | 72 | func (mock *SUSEConnectDataMock) locations() []string { 73 | if locationShouldBeFound { 74 | return []string{"testdata/suseconnect.txt"} 75 | } 76 | 77 | return []string{"testdata/notfound.txt"} 78 | } 79 | 80 | func (mock *SUSEConnectDataMock) onLocationsNotFound() bool { 81 | return mock.data.onLocationsNotFound() 82 | } 83 | 84 | func (mock *SUSEConnectDataMock) separator() byte { 85 | return mock.data.separator() 86 | } 87 | 88 | func (mock *SUSEConnectDataMock) setValues(key, value string) { 89 | mock.data.setValues(key, value) 90 | } 91 | 92 | func (mock *SUSEConnectDataMock) afterParseCheck() error { 93 | return mock.data.afterParseCheck() 94 | } 95 | 96 | func TestIntegrationSUSEConnectData(t *testing.T) { 97 | var data SUSEConnectData 98 | locationShouldBeFound = true 99 | mock := SUSEConnectDataMock{data: &data} 100 | 101 | err := ReadConfiguration(&mock) 102 | if err != nil { 103 | t.Fatal("This should've been a successful run") 104 | } 105 | 106 | if mock.data.SccURL != "https://smt.test.lan" { 107 | t.Fatal("Unexpected URL value") 108 | } 109 | 110 | if !mock.data.Insecure { 111 | t.Fatal("Unexpected Insecure value") 112 | } 113 | } 114 | 115 | func TestLocationsNotFound(t *testing.T) { 116 | var data SUSEConnectData 117 | locationShouldBeFound = false 118 | mock := SUSEConnectDataMock{data: &data} 119 | 120 | err := ReadConfiguration(&mock) 121 | if err != nil { 122 | t.Fatal("This should've been a successful run") 123 | } 124 | 125 | if mock.data.SccURL != "https://scc.suse.com" { 126 | t.Fatal("It should've been scc.suse.com") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /internal/credentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "bytes" 19 | "log" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestCredentials(t *testing.T) { 25 | cr := &Credentials{} 26 | 27 | if cr.separator() != '=' { 28 | t.Fatal("Wrong separator") 29 | } 30 | 31 | prepareLogger() 32 | err := cr.afterParseCheck() 33 | msg := "Can't find username" 34 | if err == nil || err.Error() != msg { 35 | t.Fatal("Wrong error") 36 | } 37 | shouldHaveLogged(t, msg) 38 | 39 | cr.setValues("username", "suse") 40 | prepareLogger() 41 | msg = "Can't find password" 42 | err = cr.afterParseCheck() 43 | if err == nil || err.Error() != msg { 44 | t.Fatal("Wrong error") 45 | } 46 | shouldHaveLogged(t, msg) 47 | 48 | cr.setValues("password", "1234") 49 | err = cr.afterParseCheck() 50 | if err != nil { 51 | t.Fatal("There should not be an error") 52 | } 53 | 54 | locs := cr.locations() 55 | if locs[0] != "/etc/zypp/credentials.d/SCCcredentials" { 56 | t.Fatal("Wrong location") 57 | } 58 | if locs[1] != "/run/secrets/SCCcredentials" { 59 | t.Fatal("Wrong location") 60 | } 61 | if locs[2] != "/run/secrets/credentials.d/SCCcredentials" { 62 | t.Fatal("Wrong location") 63 | } 64 | 65 | // It should log a proper warning. 66 | buffer := bytes.NewBuffer([]byte{}) 67 | log.SetOutput(buffer) 68 | cr.setValues("unknown", "value") 69 | if !strings.Contains(buffer.String(), "Warning: Unknown key 'unknown'") { 70 | t.Fatal("Wrong warning!") 71 | } 72 | } 73 | 74 | // In the following test we will create a mock that just wraps up the 75 | // `Credentials` struct and replaces its `location` function for something that 76 | // can be tested. We test for a successful run, since all the possible errors 77 | // have already been tested in the `configuration_test.go` file. 78 | 79 | type CredentialsMock struct { 80 | cr *Credentials 81 | } 82 | 83 | func (mock *CredentialsMock) locations() []string { 84 | return []string{"testdata/credentials.txt"} 85 | } 86 | 87 | func (mock *CredentialsMock) onLocationsNotFound() bool { 88 | return mock.cr.onLocationsNotFound() 89 | } 90 | 91 | func (mock *CredentialsMock) separator() byte { 92 | return mock.cr.separator() 93 | } 94 | 95 | func (mock *CredentialsMock) setValues(key, value string) { 96 | mock.cr.setValues(key, value) 97 | } 98 | 99 | func (mock *CredentialsMock) afterParseCheck() error { 100 | return mock.cr.afterParseCheck() 101 | } 102 | 103 | func TestIntegrationCredentials(t *testing.T) { 104 | var credentials Credentials 105 | mock := CredentialsMock{cr: &credentials} 106 | 107 | err := ReadConfiguration(&mock) 108 | if err != nil { 109 | t.Fatal("This should've been a successful run") 110 | } 111 | 112 | if mock.cr.Username != "SCC_a6994b1d3ae14b35agc7cef46b4fff9a" { 113 | t.Fatal("Unexpected name value") 114 | } 115 | 116 | if mock.cr.Password != "10yb1x6bd159g741ad420fd5aa5083e4" { 117 | t.Fatal("Unexpected password value") 118 | } 119 | 120 | if mock.cr.SystemToken != "36531d07-a283-441b-a02a-1cd9a88b0d5d" { 121 | t.Fatal("Unexpected system_token value") 122 | } 123 | 124 | if mock.cr.onLocationsNotFound() { 125 | t.Fatalf("It should've been false") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/subscriptions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "crypto/tls" 19 | "encoding/json" 20 | "io" 21 | "log" 22 | "net/http" 23 | "net/url" 24 | "strings" 25 | ) 26 | 27 | // Subscription has all the information that we need for SLE subscriptions. 28 | type Subscription struct { 29 | RegCode string `json:"regcode"` 30 | Status string `json:"status"` 31 | } 32 | 33 | // Request registration codes to the registration server. The `data` and the 34 | // `credentials` parameters are used in order to establish the connection with 35 | // the registration server. The `installed` parameter contains the product to 36 | // be requested. 37 | // This function uses SCC's "/connect/systems/subscriptions" API 38 | func requestRegcodes(data SUSEConnectData, credentials Credentials) ([]string, error) { 39 | var codes []string 40 | 41 | req, err := http.NewRequest("GET", data.SccURL, nil) 42 | if err != nil { 43 | return codes, loggedError(NetworkError, "Could not connect with registration server: %v\n", err) 44 | } 45 | 46 | req.URL.Path = "/connect/systems/subscriptions" 47 | req.URL.User = url.UserPassword(credentials.Username, credentials.Password) 48 | 49 | if credentials.SystemToken != "" { 50 | req.Header.Add("System-Token", credentials.SystemToken) 51 | } 52 | 53 | client := &http.Client{ 54 | Transport: &http.Transport{ 55 | TLSClientConfig: &tls.Config{ 56 | InsecureSkipVerify: data.Insecure, 57 | }, 58 | Proxy: http.ProxyFromEnvironment, 59 | }, 60 | } 61 | 62 | resp, err := client.Do(req) 63 | if err != nil { 64 | return codes, err 65 | } 66 | 67 | if resp.StatusCode == 404 { 68 | // We cannot request regcodes from a SMT server. 69 | // It does not have this API. Just return an empty string 70 | log.Println("Cannot fetch regcodes. Assuming it is SMT server") 71 | codes = append(codes, "") 72 | 73 | return codes, nil 74 | } 75 | 76 | if resp.StatusCode != 200 { 77 | return codes, loggedError(SubscriptionServerError, "Unexpected error while retrieving regcode: %s", resp.Status) 78 | } 79 | 80 | subscriptions, err := parseSubscriptions(resp.Body) 81 | if err != nil { 82 | return codes, err 83 | } 84 | 85 | for _, subscription := range subscriptions { 86 | if strings.ToUpper(subscription.Status) != "EXPIRED" { 87 | codes = append(codes, subscription.RegCode) 88 | } else { 89 | loggedError(SubscriptionServerError, "Skipping regCode: %s -- expired.", subscription.RegCode) 90 | } 91 | } 92 | 93 | return codes, err 94 | } 95 | 96 | // Parse the product as expected from the given reader. This function already 97 | // checks whether the given reader is valid or not. 98 | func parseSubscriptions(reader io.Reader) ([]Subscription, error) { 99 | var subscriptions []Subscription 100 | 101 | data, err := io.ReadAll(reader) 102 | if err != nil { 103 | return subscriptions, loggedError(SubscriptionError, "Can't read subscriptions information: %v", err.Error()) 104 | } 105 | 106 | err = json.Unmarshal(data, &subscriptions) 107 | if err != nil { 108 | return subscriptions, loggedError(SubscriptionError, "Can't read subscription: %v", err.Error()) 109 | } 110 | 111 | if len(subscriptions) == 0 { 112 | return subscriptions, loggedError(SubscriptionError, "Got 0 subscriptions") 113 | } 114 | 115 | return subscriptions, nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/regionsrv/hostsfile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "fmt" 19 | "math/rand" 20 | "os" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | // copyHostFileToTemp copies the hosts file from the fixtures directory into a 26 | // file that is meant to be temporary and that has a randomized name. Use this 27 | // temporary file instead of the original hosts file inside of the fixtures path 28 | // on these tests. 29 | func copyHostFileToTemp(mode os.FileMode) string { 30 | data, err := os.ReadFile(fixturesPath("hosts")) 31 | if err != nil { 32 | fmt.Printf("Read file error: %v\n", err) 33 | return "" 34 | } 35 | 36 | path := fixturesPath(fmt.Sprintf("testfile%v", rand.Int())) 37 | err = os.WriteFile(path, data, mode) 38 | if err != nil { 39 | fmt.Printf("Write file error: %v\n", err) 40 | return "" 41 | } 42 | 43 | return path 44 | } 45 | 46 | // Test suite below 47 | 48 | func TestUpdateHostsFileCouldNotRead(t *testing.T) { 49 | hostsFile = "/bubblegloop-swamp" 50 | err := UpdateHostsFile("hostname", "1.1.1.1") 51 | 52 | if err == nil || !strings.Contains(err.Error(), "can't read /bubblegloop-swamp file") { 53 | t.Fatalf("Expected 'can't read /bubblegloop-swamp file', got '%v'\n", err) 54 | } 55 | } 56 | 57 | func TestUpdateHostsFileCouldNotWrite(t *testing.T) { 58 | hostsFile = copyHostFileToTemp(0o400) 59 | if hostsFile == "" { 60 | t.Fatalf("Failed to initialize hosts file") 61 | } 62 | 63 | defer os.Remove(hostsFile) 64 | 65 | err := UpdateHostsFile("hostname", "1.1.1.1") 66 | if err == nil || !strings.Contains(err.Error(), "can't write") { 67 | t.Fatalf("Expected a write error, got '%v'\n", err) 68 | } 69 | } 70 | 71 | func TestUpdateHostsFileSuccessful(t *testing.T) { 72 | hostsFile = copyHostFileToTemp(0o644) 73 | if hostsFile == "" { 74 | t.Fatalf("Failed to initialize hosts file") 75 | } 76 | 77 | defer os.Remove(hostsFile) 78 | 79 | before, err := os.ReadFile(hostsFile) 80 | if err != nil { 81 | t.Fatalf("os.ReadFile failed with: %v", err) 82 | } 83 | 84 | err = UpdateHostsFile("test-hostname", "1.1.1.1") 85 | if err != nil { 86 | t.Fatalf("Expected a nil error, got: %v", err) 87 | } 88 | 89 | after, err := os.ReadFile(hostsFile) 90 | if err != nil { 91 | t.Fatalf("Expected a nil error, got: %v", err) 92 | } 93 | if !strings.Contains(string(after), string(before)) { 94 | t.Fatalf("%v\nshould contain\n%v", string(after), string(before)) 95 | } 96 | 97 | expected := "1.1.1.1 test-hostname test-hostname" 98 | if !strings.Contains(string(after), expected) { 99 | t.Fatalf("%v\nshould contain\n%v", string(after), expected) 100 | } 101 | } 102 | 103 | func TestUpdateHostsFileUpdateExistingEntry(t *testing.T) { 104 | hostsFile = copyHostFileToTemp(0o644) 105 | if hostsFile == "" { 106 | t.Fatalf("Failed to initialize hosts file") 107 | } 108 | 109 | defer os.Remove(hostsFile) 110 | 111 | err := UpdateHostsFile("ip6-localnet", "1.1.1.1") 112 | if err != nil { 113 | t.Fatalf("Expected a nil error, got: %v", err) 114 | } 115 | 116 | after, err := os.ReadFile(hostsFile) 117 | if err != nil { 118 | t.Fatalf("Expected a nil error, got: %v", err) 119 | } 120 | 121 | expected := "1.1.1.1 ip6-localnet ip6-localnet" 122 | if !strings.Contains(string(after), expected) { 123 | t.Fatalf("%v\nshould contain\n%v", string(after), expected) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/regionsrv/ca_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "crypto/sha256" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "math/rand" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | ) 27 | 28 | // testCommand implements the commander interface with an attribute for mocking 29 | // purposes. 30 | type testCommand struct { 31 | shouldFail bool 32 | } 33 | 34 | // Returns an error if `shouldFail` is set to true and nil otherwise. 35 | func (t testCommand) Run() error { 36 | if t.shouldFail { 37 | return errors.New("I AM ERROR") 38 | } 39 | return nil 40 | } 41 | 42 | // Run this before each test to get the fixtures path right. 43 | func beforeTest() { 44 | hashFilePath = fixturesPath("valid.sha256") 45 | caFilePath = fixturesPath("valid.pem") 46 | } 47 | 48 | // Returns the full path to the given fixture file. 49 | func fixturesPath(file string) string { 50 | path, _ := os.Getwd() 51 | return filepath.Join(path, "fixtures", file) 52 | } 53 | 54 | // Tests start here 55 | 56 | func TestNoUpdateIsNeeded(t *testing.T) { 57 | beforeTest() 58 | 59 | if updateNeeded("valid") { 60 | t.Fatal("Should not be needed") 61 | } 62 | 63 | if !updateNeeded("nope") { 64 | t.Fatal("Should be needed") 65 | } 66 | } 67 | 68 | func TestUpdateIsNeededNoFile(t *testing.T) { 69 | beforeTest() 70 | 71 | hashFilePath = "/tmp/wubalubadubdub" 72 | b := updateNeeded("thing") 73 | if !b { 74 | t.Fatal("Expected update to be needed") 75 | } 76 | } 77 | 78 | func TestUpdateIsNeededCouldNotReadFile(t *testing.T) { 79 | beforeTest() 80 | 81 | hashFilePath = "/proc/1/mem" 82 | b := updateNeeded("thing") 83 | if !b { 84 | t.Fatal("Expected update to be needed") 85 | } 86 | } 87 | 88 | func TestSaveCAFileBadWrite(t *testing.T) { 89 | beforeTest() 90 | 91 | hashFilePath = fixturesPath(fmt.Sprintf("file%v.sha256", rand.Int())) 92 | caFilePath = "/wubalubadubdub" 93 | cmd := testCommand{shouldFail: false} 94 | 95 | err := saveCAFile(cmd, "valid") 96 | os.Remove(hashFilePath) 97 | os.Remove(caFilePath) 98 | 99 | if err == nil { 100 | t.Fatal("Should've failed") 101 | } 102 | } 103 | 104 | func TestSaveCAFileBadCommand(t *testing.T) { 105 | beforeTest() 106 | 107 | hashFilePath = fixturesPath(fmt.Sprintf("file%v.sha256", rand.Int())) 108 | caFilePath = fixturesPath(fmt.Sprintf("file%v.pem", rand.Int())) 109 | cmd := testCommand{shouldFail: true} 110 | 111 | err := saveCAFile(cmd, "valid") 112 | os.Remove(hashFilePath) 113 | os.Remove(caFilePath) 114 | 115 | if err == nil { 116 | t.Fatal("Expected error to be non-nil\n") 117 | } 118 | 119 | if err.Error() != "I AM ERROR" { 120 | t.Fatalf("Expected another error, got: %v\n", err) 121 | } 122 | } 123 | 124 | func TestSaveCAFileSuccess(t *testing.T) { 125 | beforeTest() 126 | 127 | hashFilePath = fixturesPath("tmp.sha256") 128 | cmd := testCommand{shouldFail: false} 129 | 130 | err := saveCAFile(cmd, "valid") 131 | if err != nil { 132 | os.Remove(hashFilePath) 133 | t.Fatalf("Expected error to be nil: %v\n", err) 134 | } 135 | 136 | b, _ := os.ReadFile(hashFilePath) 137 | os.Remove(hashFilePath) 138 | 139 | hash := sha256.New() 140 | io.WriteString(hash, "valid") 141 | if string(b) != string(hash.Sum(nil)) { 142 | t.Fatal("Bad checksum") 143 | } 144 | 145 | b, _ = os.ReadFile(caFilePath) 146 | if string(b) != "valid" { 147 | t.Fatalf("Wrong certificate. Expected 'valid', got '%v'\n", string(b)) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /internal/configuration.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "bufio" 19 | "io" 20 | "os" 21 | "strings" 22 | ) 23 | 24 | // The Configuration interface allows us to fetch information from 25 | // configuration files located somewhere in the system and that follow a 26 | // grammar like: 'key' 'separator' 'value'. 27 | type Configuration interface { 28 | // Returns the character separating the key from the value. 29 | separator() byte 30 | 31 | // Returns a slice of possible locations for the configuration file. Note 32 | // that the order matters: the first elements will be evaluated first 33 | // during parse time. 34 | locations() []string 35 | 36 | // This function will be called if this app could not found any file on the 37 | // specified locations. Returns a bool: true if it has been handled by the 38 | // implementer of this interface, false if it should fail. 39 | onLocationsNotFound() bool 40 | 41 | // Called after a line with relevant information has been successfully 42 | // parsed. The key and the value are guaranteed to be already trimmed 43 | // by the caller. 44 | setValues(key, value string) 45 | 46 | // Checks that should be done after the parsing has been performed. It's 47 | // assumed that the error returned has already been logged. 48 | afterParseCheck() error 49 | } 50 | 51 | // From the given slice of locations, return the first location that actually 52 | // exists on the system. It returns an empty string on error. 53 | func getLocationPath(locations []string) string { 54 | for _, path := range locations { 55 | if _, err := os.Stat(path); err == nil { 56 | return path 57 | } 58 | } 59 | 60 | return "" 61 | } 62 | 63 | // ReadConfiguration reads the configuration and updates the given object. 64 | func ReadConfiguration(config Configuration) error { 65 | path := getLocationPath(config.locations()) 66 | if path == "" { 67 | // Leave early if locations could not be found but it can be handled by 68 | // the implementer. 69 | if config.onLocationsNotFound() { 70 | return nil 71 | } 72 | 73 | return loggedError(CredentialsNotFoundError, "SUSE Credentials not found at %v. Skipping automatic handling of repositories.", config.locations()) 74 | } 75 | 76 | file, err := os.Open(path) 77 | if err != nil { 78 | return loggedError(CredentialsNotFoundError, "Can't open %s file: %v", path, err.Error()) 79 | } 80 | defer file.Close() 81 | 82 | return parse(config, file) 83 | } 84 | 85 | // Parses the contents given by the reader and updated the given configuration. 86 | func parse(config Configuration, reader io.Reader) error { 87 | scanner := bufio.NewScanner(reader) 88 | 89 | for scanner.Scan() { 90 | // Comments & empty lines. 91 | if strings.IndexAny(scanner.Text(), "#-") == 0 { 92 | continue 93 | } 94 | 95 | if scanner.Text() == "" { 96 | continue 97 | } 98 | 99 | line := scanner.Text() 100 | // Each line should be constructed as 'key' 'separator' 'value'. 101 | parts := strings.SplitN(line, string(config.separator()), 2) 102 | 103 | if len(parts) != 2 { 104 | return loggedError(InvalidCredentialsError, "Can't parse line: %v", line) 105 | } 106 | 107 | // And finally trim the key and the value and pass it to the config. 108 | key, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) 109 | config.setValues(key, value) 110 | } 111 | 112 | // Final checks. 113 | if err := scanner.Err(); err != nil { 114 | return loggedError(InvalidCredentialsError, "Error when scanning configuration: %v", err) 115 | } 116 | 117 | if err := config.afterParseCheck(); err != nil { 118 | return err 119 | } 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/regionsrv/zypper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "os" 19 | "strings" 20 | "testing" 21 | 22 | "github.com/mssola/capture" 23 | ) 24 | 25 | func TestParseStdinSuccessful(t *testing.T) { 26 | content := []byte("RESOLVEURL\nkey:value1\nanother:value2") 27 | tmp, err := os.CreateTemp("", "file") 28 | if err != nil { 29 | t.Fatalf("Initialization error: %v", err) 30 | } 31 | 32 | defer func() { 33 | tmp.Close() 34 | os.Remove(tmp.Name()) 35 | }() 36 | 37 | if _, err := tmp.Write(content); err != nil { 38 | t.Fatalf("Initialization error: %v", err) 39 | } 40 | 41 | if _, err := tmp.Seek(0, 0); err != nil { 42 | t.Fatalf("Initialization error: %v", err) 43 | } 44 | 45 | // stdin switcheroo 46 | old := os.Stdin 47 | defer func() { 48 | os.Stdin = old 49 | }() 50 | os.Stdin = tmp 51 | 52 | params, err := ParseStdin() 53 | if err != nil { 54 | t.Fatalf("ParseStdin returned an error: %v", err) 55 | } 56 | 57 | if len(params) != 2 { 58 | t.Fatalf("There should be two entries, %v instead", len(params)) 59 | } 60 | 61 | if params["key"] != "value1" { 62 | t.Fatalf("Expected 'key' to contain 'value1', got '%v' instead", params["key"]) 63 | } 64 | 65 | if params["another"] != "value2" { 66 | t.Fatalf("Expected 'key' to contain 'value2', got '%v' instead", params["another"]) 67 | } 68 | } 69 | 70 | func TestParseStdinBadInput(t *testing.T) { 71 | old := os.Stdin 72 | defer func() { 73 | os.Stdin = old 74 | }() 75 | os.Stdin = nil 76 | 77 | _, err := ParseStdin() 78 | if err.Error() != "invalid argument" { 79 | t.Fatalf("Expected there to be an invalid argument error, got '%v' instead", err) 80 | } 81 | } 82 | 83 | func TestPrintResponseBadResponseFromServer(t *testing.T) { 84 | withSuppressedLog(func() { 85 | ts := &testServer{ 86 | bootstrapped: make(chan bool, 1), 87 | response: ContainerBuildConfig{}, 88 | } 89 | defer ts.close() 90 | 91 | go ts.run() 92 | <-ts.bootstrapped 93 | 94 | params := map[string]string{} 95 | 96 | err := PrintResponse(params) 97 | if err == nil || err.Error() != "empty response from the server" { 98 | t.Fatalf("expecting an error from ReadConfigFromServer, got %v", err) 99 | } 100 | }) 101 | } 102 | 103 | func TestPrintResponseNoCredentials(t *testing.T) { 104 | withSuppressedLog(func() { 105 | ts := &testServer{ 106 | bootstrapped: make(chan bool, 1), 107 | response: ContainerBuildConfig{ 108 | InstanceData: "instance data", 109 | }, 110 | } 111 | defer ts.close() 112 | 113 | go ts.run() 114 | <-ts.bootstrapped 115 | 116 | params := map[string]string{} 117 | 118 | err := PrintResponse(params) 119 | if err == nil || err.Error() != "no credentials given" { 120 | t.Fatalf("expecting a 'no credentials given' error, got %v", err) 121 | } 122 | }) 123 | } 124 | 125 | func TestPrintFromConfiguration(t *testing.T) { 126 | withSuppressedLog(func() { 127 | res := capture.All(func() { 128 | printFromConfiguration("/path", &ContainerBuildConfig{ 129 | InstanceData: "instance data", 130 | ServerFqdn: "test.fqdn.com", 131 | ServerIP: "1.1.1.1", 132 | Username: "banjo", 133 | Password: "kazooie", 134 | Ca: "ca", 135 | }) 136 | }) 137 | 138 | lines := strings.Split(string(res.Stdout), "\n") 139 | expected := []string{ 140 | "RESOLVEDURL", 141 | "X-Instance-Data:instance data", 142 | "", 143 | "https://banjo:kazooie@test.fqdn.com/path\x00", 144 | } 145 | 146 | if len(lines) != len(expected) { 147 | t.Fatalf("Expected %v lines, got %v", len(expected), len(lines)) 148 | } 149 | 150 | for k, v := range expected { 151 | if lines[k] != v { 152 | t.Fatalf("Expected '%v', got '%v'", v, lines[k]) 153 | } 154 | } 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /internal/configuration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "errors" 19 | "os" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestGetLocationPath(t *testing.T) { 25 | path := getLocationPath([]string{}) 26 | if path != "" { 27 | t.Fatal("It should be empty") 28 | } 29 | 30 | strs := []string{ 31 | "does/not/exist", 32 | "testdata/products-sle12.json", 33 | } 34 | 35 | path = getLocationPath(strs) 36 | if path != "testdata/products-sle12.json" { 37 | t.Fatalf("Wrong location path: %v", path) 38 | } 39 | } 40 | 41 | type NotFoundConfiguration struct{} 42 | 43 | func (cfg NotFoundConfiguration) separator() byte { 44 | return '.' 45 | } 46 | 47 | func (cfg NotFoundConfiguration) locations() []string { 48 | return []string{} 49 | } 50 | 51 | func (cfg NotFoundConfiguration) onLocationsNotFound() bool { 52 | return false 53 | } 54 | 55 | func (cfg NotFoundConfiguration) setValues(key, value string) { 56 | } 57 | 58 | func (cfg NotFoundConfiguration) afterParseCheck() error { 59 | return nil 60 | } 61 | 62 | func TestNotFound(t *testing.T) { 63 | var cfg NotFoundConfiguration 64 | 65 | prepareLogger() 66 | 67 | err := ReadConfiguration(&cfg) 68 | if err == nil || err.Error() != "SUSE Credentials not found at []. Skipping automatic handling of repositories." { 69 | t.Fatalf("Wrong error: %v", err) 70 | } 71 | 72 | shouldHaveLogged(t, "SUSE Credentials not found at []. Skipping automatic handling of repositories.") 73 | } 74 | 75 | type NotAllowedConfiguration struct{} 76 | 77 | func (cfg NotAllowedConfiguration) separator() byte { 78 | return '.' 79 | } 80 | 81 | func (cfg NotAllowedConfiguration) locations() []string { 82 | return []string{"/etc/shadow"} 83 | } 84 | 85 | func (cfg NotAllowedConfiguration) onLocationsNotFound() bool { 86 | return false 87 | } 88 | 89 | func (cfg NotAllowedConfiguration) setValues(key, value string) { 90 | } 91 | 92 | func (cfg NotAllowedConfiguration) afterParseCheck() error { 93 | return nil 94 | } 95 | 96 | func TestNotAllowed(t *testing.T) { 97 | var cfg NotAllowedConfiguration 98 | 99 | prepareLogger() 100 | 101 | err := ReadConfiguration(&cfg) 102 | msg := "Can't open /etc/shadow file: open /etc/shadow: permission denied" 103 | if err == nil || err.Error() != msg { 104 | t.Fatal("Wrong error") 105 | } 106 | 107 | shouldHaveLogged(t, msg) 108 | } 109 | 110 | func TestParseInvalid(t *testing.T) { 111 | var cfg NotAllowedConfiguration 112 | 113 | file, err := os.Open("/etc/shadow") 114 | if err == nil { 115 | file.Close() 116 | t.Fatal("There should be an error here") 117 | } 118 | 119 | prepareLogger() 120 | 121 | err = parse(cfg, file) 122 | msg := "Error when scanning configuration: invalid argument" 123 | if err == nil || err.Error() != msg { 124 | t.Fatal("Wrong error") 125 | } 126 | 127 | shouldHaveLogged(t, msg) 128 | } 129 | 130 | type ErrorAfterParseConfiguration struct{} 131 | 132 | func (cfg ErrorAfterParseConfiguration) separator() byte { 133 | return '.' 134 | } 135 | 136 | func (cfg ErrorAfterParseConfiguration) locations() []string { 137 | return []string{} 138 | } 139 | 140 | func (cfg ErrorAfterParseConfiguration) onLocationsNotFound() bool { 141 | return false 142 | } 143 | 144 | func (cfg ErrorAfterParseConfiguration) setValues(key, value string) { 145 | } 146 | 147 | func (cfg ErrorAfterParseConfiguration) afterParseCheck() error { 148 | return errors.New("I'm grumpy, and I want to error") 149 | } 150 | 151 | func TestParseFailAfterCheck(t *testing.T) { 152 | var cfg ErrorAfterParseConfiguration 153 | 154 | str := strings.NewReader("") 155 | err := parse(cfg, str) 156 | if err == nil || err.Error() != "I'm grumpy, and I want to error" { 157 | t.Fatal("Wrong error") 158 | } 159 | } 160 | 161 | func TestParseFailNoSeparator(t *testing.T) { 162 | var cfg ErrorAfterParseConfiguration 163 | 164 | str := strings.NewReader("keywithoutvalue") 165 | 166 | prepareLogger() 167 | 168 | err := parse(cfg, str) 169 | msg := "Can't parse line: keywithoutvalue" 170 | if err == nil || err.Error() != msg { 171 | t.Fatal("Wrong error") 172 | } 173 | 174 | shouldHaveLogged(t, msg) 175 | } 176 | -------------------------------------------------------------------------------- /internal/regionsrv/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package regionsrv 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "log" 23 | "net" 24 | "os" 25 | "strings" 26 | "testing" 27 | ) 28 | 29 | // testServer implements a net.Listener with some mocking attributes. 30 | type testServer struct { 31 | server net.Listener 32 | bootstrapped chan bool 33 | badResponse bool 34 | response ContainerBuildConfig 35 | } 36 | 37 | // run this testServer by taking the mocking attributes into account. 38 | func (ts *testServer) run() (err error) { 39 | ts.server, err = net.Listen("tcp", "0.0.0.0:7956") 40 | if err != nil { 41 | err = fmt.Errorf("could not start test server: %v", err) 42 | ts.bootstrapped <- true 43 | 44 | return 45 | } 46 | 47 | for { 48 | ts.bootstrapped <- true 49 | 50 | conn, err := ts.server.Accept() 51 | if err != nil { 52 | break 53 | } 54 | 55 | if conn == nil { 56 | err = errors.New("could not create connection") 57 | break 58 | } 59 | 60 | if ts.badResponse { 61 | io.WriteString(conn, "{") 62 | } 63 | 64 | b, err := json.Marshal(ts.response) 65 | if err != nil { 66 | break 67 | } 68 | 69 | io.WriteString(conn, string(b)) 70 | conn.Close() 71 | } 72 | 73 | return 74 | } 75 | 76 | // close the server if needed. 77 | func (ts *testServer) close() error { 78 | if ts.server == nil { 79 | return nil 80 | } 81 | 82 | return ts.server.Close() 83 | } 84 | 85 | // Execute fn by suppressing any logger output. 86 | func withSuppressedLog(fn func()) { 87 | log.SetOutput(io.Discard) 88 | fn() 89 | log.SetOutput(os.Stdout) 90 | } 91 | 92 | // Test suite below 93 | 94 | func TestReadConfigFromServerFailConnection(t *testing.T) { 95 | withSuppressedLog(func() { 96 | _, err := ReadConfigFromServer() 97 | if !strings.Contains(err.Error(), "connection refused") { 98 | t.Fatalf("should be a connection refused error, got '%v'", err) 99 | } 100 | }) 101 | } 102 | 103 | func TestReadConfigFromServerBadResponse(t *testing.T) { 104 | withSuppressedLog(func() { 105 | ts := &testServer{ 106 | bootstrapped: make(chan bool, 1), 107 | badResponse: true, 108 | } 109 | defer ts.close() 110 | 111 | go ts.run() 112 | <-ts.bootstrapped 113 | 114 | _, err := ReadConfigFromServer() 115 | if !strings.Contains(err.Error(), "invalid character '{' looking for beginning of object key string") { 116 | t.Fatalf("should be a 'invalid character '{' looking for beginning of object key string', got '%v'", err) 117 | } 118 | }) 119 | } 120 | 121 | func TestEmptyResponseFromServer(t *testing.T) { 122 | withSuppressedLog(func() { 123 | ts := &testServer{ 124 | bootstrapped: make(chan bool, 1), 125 | response: ContainerBuildConfig{}, 126 | } 127 | defer ts.close() 128 | 129 | go ts.run() 130 | <-ts.bootstrapped 131 | 132 | _, err := ReadConfigFromServer() 133 | if !strings.Contains(err.Error(), "empty response from the server") { 134 | t.Fatalf("should be a 'empty response from the server', got '%v'", err) 135 | } 136 | }) 137 | } 138 | 139 | func TestValidResponse(t *testing.T) { 140 | withSuppressedLog(func() { 141 | ts := &testServer{ 142 | bootstrapped: make(chan bool, 1), 143 | response: ContainerBuildConfig{ 144 | InstanceData: "instance data", 145 | }, 146 | } 147 | defer ts.close() 148 | 149 | go ts.run() 150 | <-ts.bootstrapped 151 | 152 | cfg, err := ReadConfigFromServer() 153 | if err != nil { 154 | t.Fatalf("should be nil but got: %v", err) 155 | } 156 | 157 | if cfg.InstanceData != ts.response.InstanceData { 158 | t.Fatalf("Expected '%v', got '%v'", cfg.InstanceData, ts.response.InstanceData) 159 | } 160 | }) 161 | } 162 | 163 | func TestServerReachableNope(t *testing.T) { 164 | withSuppressedLog(func() { 165 | err := ServerReachable() 166 | if !strings.Contains(err.Error(), "connection refused") { 167 | t.Fatalf("should be a connection refused error, got '%v'", err) 168 | } 169 | }) 170 | } 171 | 172 | func TestServerReachableSuccessful(t *testing.T) { 173 | withSuppressedLog(func() { 174 | ts := &testServer{bootstrapped: make(chan bool, 1)} 175 | defer ts.close() 176 | 177 | go ts.run() 178 | <-ts.bootstrapped 179 | 180 | err := ServerReachable() 181 | if err != nil { 182 | t.Fatalf("should be nil but got: %v", err) 183 | } 184 | }) 185 | } 186 | -------------------------------------------------------------------------------- /internal/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "log" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestGetLogWriterFromRegularFile(t *testing.T) { 29 | tempFile, err := os.CreateTemp("", "prefix") 30 | assert.Nil(t, err) 31 | 32 | defer os.Remove(tempFile.Name()) 33 | 34 | w, err := getLogWriter(tempFile.Name()) 35 | assert.Nil(t, err) 36 | assert.NotNil(t, w) 37 | w.Close() 38 | } 39 | 40 | func TestGetLogWriterFromRelativeFile(t *testing.T) { 41 | w, err := getLogWriter("test.log") 42 | assert.EqualError(t, err, "log path is not absolute: test.log") 43 | assert.Nil(t, w) 44 | } 45 | 46 | func TestGetLogWriterFromValidDir(t *testing.T) { 47 | dir, err := os.MkdirTemp("", "") 48 | assert.Nil(t, err) 49 | defer os.RemoveAll(dir) 50 | 51 | w, err := getLogWriter(dir) 52 | assert.EqualError(t, err, fmt.Sprintf("open %s: is a directory", dir)) 53 | assert.Nil(t, w) 54 | } 55 | 56 | func TestGetLogWriterFromMissingDir(t *testing.T) { 57 | dir, err := os.MkdirTemp("", "") 58 | assert.Nil(t, err) 59 | defer os.RemoveAll(dir) 60 | 61 | // ensure the path is terminated by / othewise it will be considered a file 62 | path := filepath.Join(dir, "does", "not", "exits") + string(filepath.Separator) 63 | w, err := getLogWriter(path) 64 | assert.EqualError(t, err, fmt.Sprintf("open %s: no such file or directory", path)) 65 | assert.Nil(t, w) 66 | } 67 | 68 | func TestGetLogWriterFromMissingDirFile(t *testing.T) { 69 | dir, err := os.MkdirTemp("", "") 70 | assert.Nil(t, err) 71 | defer os.RemoveAll(dir) 72 | 73 | path := filepath.Join(dir, "does", "not", "exits", "test.log") 74 | w, err := getLogWriter(path) 75 | assert.EqualError(t, err, fmt.Sprintf("open %s: no such file or directory", path)) 76 | assert.Nil(t, w) 77 | } 78 | 79 | func TestGetLogWriterFromEmptyString(t *testing.T) { 80 | w, err := getLogWriter("") 81 | assert.EqualError(t, err, "path is empty") 82 | assert.Nil(t, w) 83 | } 84 | 85 | func TestGetLogWriterFromSpaces(t *testing.T) { 86 | w, err := getLogWriter(" ") 87 | assert.EqualError(t, err, "path is empty") 88 | assert.Nil(t, w) 89 | } 90 | 91 | func TestGetLogWriterFromSymbolInName(t *testing.T) { 92 | dir, err := os.MkdirTemp("", "") 93 | assert.Nil(t, err) 94 | defer os.RemoveAll(dir) 95 | 96 | w, err := getLogWriter(filepath.Join(dir, "@")) 97 | assert.Nil(t, err) 98 | assert.NotNil(t, w) 99 | w.Close() 100 | } 101 | 102 | func TestGetLogWriterFromDevice(t *testing.T) { 103 | w, err := getLogWriter("/dev/null") 104 | assert.EqualError(t, err, "path is not a regular file: /dev/null") 105 | assert.Nil(t, w) 106 | } 107 | 108 | func TestGetLogWriterFromNonWritableFile(t *testing.T) { 109 | // /proc/1/mem is a valid regular file, but 110 | // it is not writable by any user, even root 111 | w, err := getLogWriter("/proc/1/mem") 112 | // it can fail with various errors 113 | // not worth checking for an exact message 114 | assert.NotNil(t, err) 115 | assert.Nil(t, w) 116 | } 117 | 118 | func TestGetLogEnvIfNotSet(t *testing.T) { 119 | // ensure no variable is set 120 | os.Unsetenv(LogEnv) 121 | path := getLogEnv() 122 | assert.Empty(t, path) 123 | } 124 | 125 | func TestGetLogEnvIfSet(t *testing.T) { 126 | // ensure no variable is set 127 | os.Unsetenv(LogEnv) 128 | err := os.Setenv(LogEnv, "/path/file.log") 129 | assert.Nil(t, err) 130 | defer os.Unsetenv(LogEnv) 131 | 132 | envPath := getLogEnv() 133 | assert.Equal(t, "/path/file.log", envPath) 134 | } 135 | 136 | func TestGetLogEnvTrimSpaces(t *testing.T) { 137 | // ensure no variable is set 138 | os.Unsetenv(LogEnv) 139 | err := os.Setenv(LogEnv, " /path/file.log ") 140 | assert.Nil(t, err) 141 | defer os.Unsetenv(LogEnv) 142 | 143 | envPath := getLogEnv() 144 | assert.Equal(t, "/path/file.log", envPath) 145 | } 146 | 147 | // Ensures that the log is always written to a file and Stderr. 148 | func TestSetLoggerOutput(t *testing.T) { 149 | // ensure no variable is set 150 | os.Unsetenv(LogEnv) 151 | 152 | tempFile, err := os.CreateTemp("", "") 153 | assert.Nil(t, err) 154 | defer os.Remove(tempFile.Name()) 155 | 156 | err = os.Setenv(LogEnv, tempFile.Name()) 157 | assert.Nil(t, err) 158 | defer os.Unsetenv(LogEnv) 159 | 160 | logLine := "This in a log entry in a file and Stderr" 161 | 162 | var stdData, fileData string 163 | 164 | stdData, err = captureStderr(t, func() { 165 | SetLoggerOutput() 166 | log.Println(logLine) 167 | }) 168 | 169 | assert.Nil(t, err, "Failed to capture Stderr") 170 | 171 | buff := new(bytes.Buffer) 172 | _, err = buff.ReadFrom(tempFile) 173 | assert.Nil(t, err, "Failed to read temp file") 174 | 175 | fileData = buff.String() 176 | 177 | assert.Contains(t, fileData, logLine) 178 | assert.Contains(t, stdData, logLine) 179 | } 180 | -------------------------------------------------------------------------------- /internal/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | "path/filepath" 22 | "regexp" 23 | "strings" 24 | ) 25 | 26 | // Convert from bool to int. 27 | func boolToInt(b bool) int { 28 | if b { 29 | return 1 30 | } 31 | return 0 32 | } 33 | 34 | // DumpRepositories dumps the repositories of the given product to the given 35 | // writer. 36 | func DumpRepositories(w io.Writer, product Product) { 37 | fmt.Fprintf(w, "# generated by container-suseconnect\n") 38 | fmt.Fprintf(w, "\n") 39 | 40 | // Always print the first product disregarding if it is recommended or not 41 | dumpRepositoriesRecursive(w, product, true) 42 | } 43 | 44 | // dumpRepositoriesRecursive prints available product repositories to the 45 | // provided writer. 46 | // 47 | // The function takes all product extensions into account, which will be 48 | // printed recursively too. 49 | // 50 | // `dumpAlways` specifies if the products repositories should be 51 | // printed ignoring if it is recommended or not. 52 | // 53 | // The user has the option to enable certain modules via its `identifier` by 54 | // setting them within the `ADDITIONAL_MODULES` environment variable. Multiple 55 | // modules can be set comma separated. 56 | func dumpRepositoriesRecursive( 57 | w io.Writer, 58 | product Product, 59 | dumpAlways bool, 60 | ) { 61 | for _, repo := range product.Repositories { 62 | if product.Recommended || dumpAlways || 63 | moduleEnabledInEnv(product.Identifier) || 64 | moduleEnabledInProductFiles(product.Identifier) { 65 | 66 | fmt.Fprintf(w, "[%s]\n", repo.Name) 67 | fmt.Fprintf(w, "name=%s\n", repo.Description) 68 | fmt.Fprintf(w, "baseurl=%s\n", repo.URL) 69 | fmt.Fprintf(w, "autorefresh=%d\n", boolToInt(repo.Autorefresh)) 70 | fmt.Fprintf(w, "enabled=%d\n", boolToInt(repo.Enabled)) 71 | fmt.Fprintf(w, "\n") 72 | } 73 | } 74 | 75 | // Continue the traversal for the extensions if needed 76 | if len(product.Extensions) > 0 { 77 | for _, extension := range product.Extensions { 78 | dumpRepositoriesRecursive(w, extension, false) 79 | } 80 | } 81 | } 82 | 83 | // moduleEnabledInEnv returns true if the provided `identifier` is included in 84 | // the `ADDITIONAL_MODULES` environment variable, otherwise false. 85 | func moduleEnabledInEnv(identifier string) bool { 86 | for _, i := range strings.Split(os.Getenv("ADDITIONAL_MODULES"), ",") { 87 | if identifier == i { 88 | return true 89 | } 90 | } 91 | 92 | return false 93 | } 94 | 95 | // moduleEnabledInProductFiles returns true if the provided `identifier` is 96 | // a name of a file in the /etc/product.d/*.prod, otherwise false. 97 | func moduleEnabledInProductFiles(identifier string) bool { 98 | files, err := os.ReadDir("/etc/products.d") 99 | if err != nil { 100 | return false 101 | } 102 | 103 | for _, file := range files { 104 | ext := filepath.Ext(file.Name()) 105 | info, err := file.Info() 106 | if err != nil { 107 | continue 108 | } 109 | 110 | if ext == ".prod" && 111 | identifier == strings.TrimSuffix(file.Name(), ext) && 112 | info.Mode()&os.ModeSymlink == 0 { 113 | return true 114 | } 115 | } 116 | 117 | return false 118 | } 119 | 120 | // ListModules prints the provided `products` slice the provided writer `w` in a 121 | // human readable way 122 | func ListModules(w io.Writer, products []Product) { 123 | for _, product := range products { 124 | if product.ProductType == "module" { 125 | fmt.Fprintf(w, "Name: %v\n", product.Name) 126 | fmt.Fprintf(w, "Identifier: %v\n", product.Identifier) 127 | fmt.Fprintf(w, "Recommended: %v\n", product.Recommended) 128 | fmt.Fprintf(w, "\n") 129 | } 130 | 131 | // Continue traversal with available product extensions 132 | ListModules(w, product.Extensions) 133 | } 134 | } 135 | 136 | // ListProducts prints the provided `products` slice the provided writer `w` in a 137 | // human readable way 138 | func ListProducts(w io.Writer, products []Product, baseProduct string) { 139 | for _, product := range products { 140 | fmt.Fprintf(w, "Name: %v\n", product.Name) 141 | fmt.Fprintf(w, "Type: %v\n", product.ProductType) 142 | fmt.Fprintf(w, "Identifier: %v\n", product.Identifier) 143 | fmt.Fprintf(w, "Based on: %v\n", baseProduct) 144 | fmt.Fprintf(w, "Recommended: %v\n", product.Recommended) 145 | 146 | // Strip HTML tags from the description 147 | r := regexp.MustCompile(`<[^>]*>\s*`) 148 | fmt.Fprintf(w, "Description: %v\n", 149 | strings.TrimSpace(r.ReplaceAllString(product.Description, ""))) 150 | 151 | fmt.Fprintf(w, "Repositories:\n") 152 | for idx, repo := range product.Repositories { 153 | repoState := "disabled" 154 | if repo.Enabled { 155 | repoState = "enabled" 156 | } 157 | fmt.Fprintf(w, "%v. %v: %v (%v)\n", idx+1, repo.Name, repo.URL, repoState) 158 | } 159 | fmt.Fprintf(w, "\n") 160 | 161 | // Continue traversal with available product extensions 162 | ListProducts(w, product.Extensions, product.Identifier) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /internal/subscriptions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "net/http/httptest" 22 | "os" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | func subscriptionHelper(t *testing.T, subscription Subscription) { 28 | if subscription.RegCode != "35098ff7" { 29 | t.Fatal("Wrong regcode for subscription") 30 | } 31 | } 32 | 33 | // Tests for the parseSubscriptions function. 34 | 35 | func TestUnreadableSubscription(t *testing.T) { 36 | file, err := os.Open("non-existant-file") 37 | if err == nil { 38 | file.Close() 39 | t.Fatal("This should've been an error...") 40 | } 41 | 42 | _, err = parseSubscriptions(file) 43 | if err == nil || err.Error() != "Can't read subscriptions information: invalid argument" { 44 | t.Fatalf("This is not the proper error we're expecting: %v", err) 45 | } 46 | } 47 | 48 | func TestInvalidJsonForSubscriptions(t *testing.T) { 49 | reader := strings.NewReader("invalid json is invalid") 50 | _, err := parseSubscriptions(reader) 51 | 52 | if err == nil || 53 | err.Error() != "Can't read subscription: invalid character 'i' looking for beginning of value" { 54 | 55 | t.Fatalf("This is not the proper error we're expecting: %v", err) 56 | } 57 | } 58 | 59 | func TestEmptySubscriptions(t *testing.T) { 60 | file, err := os.Open("testdata/empty-subscriptions.json") 61 | if err != nil { 62 | t.Fatal("Something went wrong when reading the JSON file") 63 | } 64 | defer file.Close() 65 | 66 | subscriptions, err := parseSubscriptions(file) 67 | if err == nil || err.Error() != "Got 0 subscriptions" { 68 | t.Fatal("Unexpected error when reading a valid JSON file") 69 | } 70 | if len(subscriptions) != 0 { 71 | t.Fatalf("It should be empty") 72 | } 73 | } 74 | 75 | func TestValidSubscriptions(t *testing.T) { 76 | file, err := os.Open("testdata/subscriptions.json") 77 | if err != nil { 78 | t.Fatal("Something went wrong when reading the JSON file") 79 | } 80 | defer file.Close() 81 | 82 | subscriptions, err := parseSubscriptions(file) 83 | if err != nil { 84 | t.Fatal("Unexpected error when reading a valid JSON file") 85 | } 86 | 87 | if len(subscriptions) != 2 { 88 | t.Fatalf("Unexpected number of subscriptions found. Got %d, expected %d", len(subscriptions), 1) 89 | } 90 | 91 | subscriptionHelper(t, subscriptions[0]) 92 | } 93 | 94 | // Tests for the requestRegcodes function. 95 | 96 | func TestInvalidRequestForRegcodes(t *testing.T) { 97 | var cr Credentials 98 | data := SUSEConnectData{SccURL: ":", Insecure: true} 99 | 100 | _, err := requestRegcodes(data, cr) 101 | if err == nil || !strings.Contains(err.Error(), "missing protocol scheme") { 102 | t.Fatalf("There should be a proper error: %v", err) 103 | } 104 | } 105 | 106 | func TestFaultyRequestForRegcodes(t *testing.T) { 107 | var cr Credentials 108 | data := SUSEConnectData{SccURL: "http://", Insecure: true} 109 | 110 | _, err := requestRegcodes(data, cr) 111 | if err == nil || !strings.Contains(err.Error(), "no Host in request URL") { 112 | t.Fatalf("There should be a proper error: %v", err) 113 | } 114 | } 115 | 116 | func TestRemoteErrorWhileRequestingRegcodes(t *testing.T) { 117 | // We setup a fake http server that mocks a registration server. 118 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 | http.Error(w, "something bad happened", 500) 120 | })) 121 | defer ts.Close() 122 | 123 | var cr Credentials 124 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 125 | 126 | _, err := requestRegcodes(data, cr) 127 | if err == nil || err.Error() != "Unexpected error while retrieving regcode: 500 Internal Server Error" { 128 | t.Fatalf("There should be a proper error: %v", err) 129 | } 130 | } 131 | 132 | func TestValidRequestForRegcodes(t *testing.T) { 133 | // We setup a fake http server that mocks a registration server. 134 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 | file, err := os.Open("testdata/subscriptions.json") 136 | if err != nil { 137 | fmt.Fprintln(w, "FAIL!") 138 | return 139 | } 140 | io.Copy(w, file) 141 | file.Close() 142 | })) 143 | defer ts.Close() 144 | 145 | var cr Credentials 146 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 147 | 148 | codes, err := requestRegcodes(data, cr) 149 | if err != nil { 150 | t.Fatal("It should've run just fine...") 151 | } 152 | 153 | // This also tests that we're not including expired regcodes 154 | if len(codes) != 1 { 155 | t.Fatalf("Unexpected number of products found. Got %d, expected %d", len(codes), 1) 156 | } 157 | 158 | if codes[0] != "35098ff7" { 159 | t.Fatalf("Got the wrong registration code: %v", codes[0]) 160 | } 161 | } 162 | 163 | func TestRequestEmptyRegcodes(t *testing.T) { 164 | // We setup a fake http server that mocks a registration server. 165 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 | file, err := os.Open("testdata/empty-subscriptions.json") 167 | if err != nil { 168 | fmt.Fprintln(w, "FAIL!") 169 | return 170 | } 171 | io.Copy(w, file) 172 | file.Close() 173 | })) 174 | defer ts.Close() 175 | 176 | var cr Credentials 177 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 178 | 179 | codes, err := requestRegcodes(data, cr) 180 | if err == nil || err.Error() != "Got 0 subscriptions" { 181 | t.Fatal("Unexpected error when reading a valid JSON file") 182 | } 183 | 184 | if len(codes) != 0 { 185 | t.Fatalf("It should be 0") 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /cmd/container-suseconnect/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Gives access to repositories during docker build and run using the host 16 | // machine credentials. 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | "strconv" 26 | "time" 27 | 28 | cs "github.com/SUSE/container-suseconnect/internal" 29 | "github.com/SUSE/container-suseconnect/internal/regionsrv" 30 | ) 31 | 32 | var logCredentialsErrors = false 33 | 34 | func init() { 35 | value := os.Getenv("CONTAINER_SUSECONNECT_LOG_CREDENTIALS_ERR") 36 | enabled, err := strconv.ParseBool(value) 37 | 38 | if err == nil && enabled { 39 | logCredentialsErrors = true 40 | } 41 | 42 | flag.BoolFunc("version", "print version and exit", func(string) error { 43 | fmt.Println(cs.GetVersion()) 44 | os.Exit(0) 45 | return nil 46 | }) 47 | 48 | flag.BoolFunc("log-credentials-errors", "log missing credentials and exit with 1", func(string) error { 49 | logCredentialsErrors = true 50 | return nil 51 | }) 52 | 53 | flag.Usage = func() { 54 | fmt.Fprintf(flag.CommandLine.Output(), 55 | "container-suseconnect: Access zypper repositories from within containers"+ 56 | " (© %d SUSE LLC)\n", time.Now().Year()) 57 | fmt.Fprintf(flag.CommandLine.Output(), 58 | ` 59 | Usage of container-suseconnect: 60 | 61 | This application can be used to retrieve basic metadata about SLES 62 | related products and module extensions. 63 | 64 | Please use the 'lp|list-products' subcommand for listing all currently 65 | available products including their repositories and a short description. 66 | 67 | Use the 'lm|list-modules' subcommand for listing available modules, where 68 | their 'Identifier' can be used to enable them via the ADDITIONAL_MODULES 69 | environment variable during container creation/run. When enabling multiple 70 | modules the identifiers are expected to be comma-separated. 71 | 72 | The 'z|zypp|zypper' subcommand runs the application as zypper plugin and is only 73 | intended to use for debugging purposes. 74 | 75 | `) 76 | flag.PrintDefaults() 77 | } 78 | } 79 | 80 | func main() { 81 | // Determine the application's action based on the binary name or command-line arguments. 82 | var appAction func() error 83 | 84 | switch filepath.Base(os.Args[0]) { 85 | case "container-suseconnect-zypp": 86 | appAction = runZypperPlugin 87 | case "susecloud": 88 | appAction = runZypperURLResolver 89 | default: 90 | flag.Parse() 91 | 92 | // Override default action based on command-line arguments. 93 | switch flag.Arg(0) { 94 | case "lp", "list-products": 95 | appAction = runListProducts 96 | case "lm", "list-modules": 97 | appAction = runListModules 98 | case "susecloud": 99 | appAction = runZypperURLResolver 100 | case "z", "zypp", "zypper": 101 | appAction = runZypperPlugin 102 | default: 103 | flag.Usage() 104 | os.Exit(1) 105 | } 106 | } 107 | 108 | // Run the application with the selected action. 109 | cs.SetLoggerOutput() 110 | if err := appAction(); err != nil { 111 | if !cs.IsCredentialsNotFoundError(err) || logCredentialsErrors { 112 | log.Fatal(err) 113 | } 114 | } 115 | } 116 | 117 | // requestProducts collects a slice of products for the currently available 118 | // environment 119 | func requestProducts() ([]cs.Product, error) { 120 | credentials := cs.Credentials{} 121 | suseConnectData := cs.SUSEConnectData{} 122 | 123 | // read config from "containerbuild-regionsrv" service, if that service is 124 | // running, we're running inside a public cloud instance in that case read 125 | // config from "mounted" files if the service is not available 126 | if err := regionsrv.ServerReachable(); err == nil { 127 | log.Printf("containerbuild-regionsrv reachable, reading config\n") 128 | 129 | cloudCfg, err := regionsrv.ReadConfigFromServer() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | credentials.Username = cloudCfg.Username 135 | credentials.Password = cloudCfg.Password 136 | credentials.InstanceData = cloudCfg.InstanceData 137 | 138 | suseConnectData.SccURL = "https://" + cloudCfg.ServerFqdn 139 | suseConnectData.Insecure = false 140 | 141 | if cloudCfg.Ca != "" { 142 | regionsrv.SaveCAFile(cloudCfg.Ca) 143 | } 144 | 145 | regionsrv.UpdateHostsFile(cloudCfg.ServerFqdn, cloudCfg.ServerIP) 146 | } else { 147 | if err := cs.ReadConfiguration(&credentials); err != nil { 148 | return nil, err 149 | } 150 | 151 | if err := cs.ReadConfiguration(&suseConnectData); err != nil { 152 | return nil, err 153 | } 154 | } 155 | 156 | installedProduct, err := cs.GetInstalledProduct() 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | log.Printf("Installed product: %v\n", installedProduct) 162 | log.Printf("Registration server set to %v\n", suseConnectData.SccURL) 163 | 164 | products, err := cs.RequestProducts(suseConnectData, credentials, installedProduct) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | return products, nil 170 | } 171 | 172 | // Read the arguments as given by zypper on the stdin and print into stdout the 173 | // response to be used. 174 | func runZypperURLResolver() error { 175 | if err := regionsrv.ServerReachable(); err != nil { 176 | return fmt.Errorf("could not reach build server from the host: %v", err) 177 | } 178 | 179 | input, err := regionsrv.ParseStdin() 180 | if err != nil { 181 | return fmt.Errorf("could not parse input: %s", err) 182 | } 183 | 184 | return regionsrv.PrintResponse(input) 185 | } 186 | 187 | // runZypperPlugin runs the application in zypper plugin mode, which dumps 188 | // all available repositories for the installed product. Additional modules 189 | // can be specified via the `ADDITIONAL_MODULES` environment variable, which 190 | // reflect the module `identifier`. 191 | func runZypperPlugin() error { 192 | products, err := requestProducts() 193 | if err != nil { 194 | return err 195 | } 196 | 197 | for _, product := range products { 198 | cs.DumpRepositories(os.Stdout, product) 199 | } 200 | 201 | return nil 202 | } 203 | 204 | // runListModules lists all available modules and their metadata, which 205 | // includes the `Name`, `Identifier` and the `Recommended` flag. 206 | func runListModules() error { 207 | products, err := requestProducts() 208 | if err != nil { 209 | return err 210 | } 211 | 212 | fmt.Printf("All available modules:\n\n") 213 | cs.ListModules(os.Stdout, products) 214 | 215 | return nil 216 | } 217 | 218 | // runListProducts lists all available products and their metadata 219 | func runListProducts() error { 220 | products, err := requestProducts() 221 | if err != nil { 222 | return err 223 | } 224 | 225 | fmt.Printf("All available products:\n\n") 226 | cs.ListProducts(os.Stdout, products, "none") 227 | 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /internal/products.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "crypto/tls" 19 | "encoding/json" 20 | "io" 21 | "log" 22 | "net/http" 23 | "net/url" 24 | ) 25 | 26 | // Repository has all the information we need from repositories as given by the 27 | // registration server. 28 | type Repository struct { 29 | Name string `json:"name"` 30 | Description string `json:"description"` 31 | URL string `json:"url"` 32 | Autorefresh bool `json:"autorefresh"` 33 | Enabled bool `json:"enabled"` 34 | } 35 | 36 | // Product has all the information we need from product as given by the registration 37 | // server. It contains a slice of repositories in it. 38 | type Product struct { 39 | ProductType string `json:"product_type"` 40 | Identifier string `json:"identifier"` 41 | Version string `json:"version"` 42 | Arch string `json:"arch"` 43 | Repositories []Repository `json:"repositories"` 44 | Extensions []Product `json:"extensions"` 45 | Recommended bool `json:"recommended"` 46 | Name string `json:"name"` 47 | Description string `json:"description"` 48 | } 49 | 50 | // Take the "Product" as returned from an RMT and adjust the repository URLs 51 | // to include a "credentials" parameter. This is somewhat specific to the RMT 52 | // instances available for Public Cloud on-demand instances, as they need to 53 | // authenticate to be able to access repositories. 54 | func fixRepoUrlsForRMT(p *Product) error { 55 | for i := range p.Repositories { 56 | repourl, err := url.Parse(p.Repositories[i].URL) 57 | if err != nil { 58 | loggedError(RepositoryError, "Unable to parse repository URL: %s - %v", p.Repositories[i].URL, err) 59 | return err 60 | } 61 | 62 | params := repourl.Query() 63 | if params.Get("credentials") == "" { 64 | params["credentials"] = []string{"SCCcredentials"} 65 | } 66 | 67 | repourl.RawQuery = params.Encode() 68 | p.Repositories[i].URL = repourl.String() 69 | } 70 | 71 | // Products can have nested Products (Extensions) handle those recursively 72 | for i := range p.Extensions { 73 | if err := fixRepoUrlsForRMT(&p.Extensions[i]); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // Parse the product as expected from the given reader. This function already 82 | // checks whether the given reader is valid or not. 83 | func parseProducts(reader io.Reader) ([]Product, error) { 84 | var products []Product 85 | 86 | data, err := io.ReadAll(reader) 87 | if err != nil { 88 | return products, 89 | loggedError(RepositoryError, "Can't read product information: %v", err.Error()) 90 | } 91 | 92 | // Depending on which API was used the JSON we get passed contains 93 | // either a list of products ("/connect/subscriptions/products") with 94 | // a single element in it or a single product 95 | // ("/connect/systems/products"). So we need to Unmarshall() slightly 96 | // different for both cases. 97 | err = json.Unmarshal(data, &products) 98 | if err != nil { 99 | products = nil 100 | // When connected to RMT we only got a single "Product", so let's try 101 | // to unmarshall that. (And add a "credential" parameter to it if that 102 | // is not present) 103 | var product Product 104 | err = json.Unmarshal(data, &product) 105 | if err == nil { 106 | fixRepoUrlsForRMT(&product) 107 | products = append(products, product) 108 | } 109 | } 110 | 111 | if err != nil { 112 | return products, 113 | loggedError(RepositoryError, "Can't read product information: %v - %s", err.Error(), data) 114 | } 115 | 116 | return products, nil 117 | } 118 | 119 | // Request product information to the registration server. The `regCode` 120 | // parameters is used to establish the connection with 121 | // the registration server. The `installed` parameter contains the product to 122 | // be requested. 123 | // This function relies on [/connect/subscriptions/products](https://github.com/SUSE/connect/wiki/SCC-API-%28Implemented%29#product) API. 124 | func requestProductsFromRegCodeOrSystem(data SUSEConnectData, regCode string, 125 | credentials Credentials, installed InstalledProduct, 126 | ) ([]Product, error) { 127 | var products []Product 128 | var err error 129 | 130 | req, err := http.NewRequest("GET", data.SccURL, nil) 131 | if err != nil { 132 | return products, loggedError(NetworkError, "Could not connect with registration server: %v\n", err) 133 | } 134 | 135 | values := req.URL.Query() 136 | values.Add("identifier", installed.Identifier) 137 | values.Add("version", installed.Version) 138 | values.Add("arch", installed.Arch) 139 | req.URL.RawQuery = values.Encode() 140 | 141 | if len(regCode) > 0 { 142 | req.Header.Add("Authorization", `Token token=`+regCode) 143 | req.URL.Path = "/connect/subscriptions/products" 144 | } else { 145 | // we're connected to a RMT, which does not provide the /connect/subscriptions/products 146 | // endpoint. Fallback to "/connect/systems/products" here. 147 | req.URL.Path = "/connect/systems/products" 148 | auth := url.UserPassword(credentials.Username, credentials.Password) 149 | req.URL.User = auth 150 | 151 | if credentials.SystemToken != "" { 152 | req.Header.Add("System-Token", credentials.SystemToken) 153 | } 154 | } 155 | 156 | client := &http.Client{ 157 | Transport: &http.Transport{ 158 | TLSClientConfig: &tls.Config{ 159 | InsecureSkipVerify: data.Insecure, 160 | }, 161 | Proxy: http.ProxyFromEnvironment, 162 | }, 163 | } 164 | 165 | resp, err := client.Do(req) 166 | if err != nil { 167 | return products, err 168 | } 169 | 170 | if resp.StatusCode != 200 { 171 | var payload map[string]interface{} 172 | dec := json.NewDecoder(resp.Body) 173 | 174 | if err := dec.Decode(&payload); err == nil { 175 | if err, ok := payload["error"]; ok { 176 | log.Println(err) 177 | } 178 | } 179 | 180 | return products, loggedError(SubscriptionServerError, "Unexpected error while retrieving products with regCode %s: %s", regCode, resp.Status) 181 | } 182 | 183 | return parseProducts(resp.Body) 184 | } 185 | 186 | // RequestProducts fetches product information to the registration server. The 187 | // `data` and the `credentials` parameters are used in order to establish the 188 | // connection with the registration server. The `installed` parameter contains 189 | // the product to be requested. 190 | func RequestProducts(data SUSEConnectData, credentials Credentials, 191 | installed InstalledProduct, 192 | ) ([]Product, error) { 193 | var products []Product 194 | var regCodes []string 195 | var err error 196 | 197 | regCodes, err = requestRegcodes(data, credentials) 198 | if err != nil { 199 | return products, err 200 | } 201 | 202 | for _, regCode := range regCodes { 203 | p, _err := requestProductsFromRegCodeOrSystem(data, regCode, credentials, installed) 204 | if _err != nil { 205 | err = _err 206 | continue 207 | } 208 | 209 | products = append(products, p...) 210 | } 211 | 212 | if len(products) > 0 { 213 | err = nil 214 | } 215 | 216 | return products, err 217 | } 218 | -------------------------------------------------------------------------------- /internal/products_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "net/http/httptest" 22 | "os" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | func productHelper(t *testing.T, product Product, expectedVersion string) { 28 | if product.ProductType != "base" { 29 | t.Fatal("Wrong base for product") 30 | } 31 | if product.Identifier != "SLES" { 32 | t.Fatal("Wrong identifier for product") 33 | } 34 | if product.Version != expectedVersion { 35 | t.Fatal("Wrong version for product") 36 | } 37 | if product.Arch != "x86_64" { 38 | t.Fatal("Wrong arch for product") 39 | } 40 | } 41 | 42 | func productHelperSLE12(t *testing.T, product Product) { 43 | productHelper(t, product, "12") 44 | 45 | if len(product.Repositories) != 4 { 46 | t.Fatalf("Wrong number of repos %v", len(product.Repositories)) 47 | } 48 | 49 | if product.Repositories[3].Name != "SLES12-Debuginfo-Pool" { 50 | t.Fatal("Unexpected value") 51 | } 52 | 53 | expectedURL := "https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product_debug" 54 | if string(product.Repositories[3].URL) != expectedURL { 55 | t.Fatalf("Unexpected repository URL: %s", product.Repositories[3].URL) 56 | } 57 | } 58 | 59 | func productHelperSLE15RMT(t *testing.T, product Product) { 60 | productHelper(t, product, "15.1") 61 | 62 | if len(product.Repositories) != 6 { 63 | t.Fatal("Wrong number of repos") 64 | } 65 | 66 | if product.Extensions[0].Repositories[2].Name != "SLE-Module-Basesystem15-SP1-Pool" { 67 | t.Fatalf("Unexpected Extension Name: %v", product.Extensions[0].Repositories[2].Name) 68 | } 69 | 70 | expectedURL := "https://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15-SP1/x86_64/product/?credentials=SCCcredentials" 71 | if string(product.Extensions[0].Repositories[2].URL) != expectedURL { 72 | t.Fatalf("Unexpected repository URL: %s", product.Extensions[0].Repositories[2].URL) 73 | } 74 | } 75 | 76 | // Tests for the parseProduct function. 77 | 78 | func TestUnreadableProduct(t *testing.T) { 79 | file, err := os.Open("non-existant-file") 80 | if err == nil { 81 | file.Close() 82 | t.Fatal("This should've been an error...") 83 | } 84 | 85 | _, err = parseProducts(file) 86 | if err == nil || err.Error() != "Can't read product information: invalid argument" { 87 | t.Fatal("This is not the proper error we're expecting") 88 | } 89 | } 90 | 91 | func TestInvalidJsonForProduct(t *testing.T) { 92 | reader := strings.NewReader("invalid json is invalid") 93 | _, err := parseProducts(reader) 94 | 95 | if err == nil || 96 | err.Error() != "Can't read product information: invalid character 'i' looking for beginning of value - invalid json is invalid" { 97 | 98 | t.Fatalf("This is not the proper error we're expecting: %v", err) 99 | } 100 | } 101 | 102 | func TestValidProduct(t *testing.T) { 103 | file, err := os.Open("testdata/products-sle12.json") 104 | if err != nil { 105 | t.Fatal("Something went wrong when reading the JSON file") 106 | } 107 | defer file.Close() 108 | 109 | products, err := parseProducts(file) 110 | if err != nil { 111 | t.Fatal("Unexpected error when reading a valid JSON file") 112 | } 113 | 114 | if len(products) != 1 { 115 | t.Fatalf("Unexpected number of products found. Got %d, expected %d", len(products), 1) 116 | } 117 | 118 | productHelperSLE12(t, products[0]) 119 | } 120 | 121 | // Tests for the requestProduct function. 122 | 123 | func TestInvalidRequestForProduct(t *testing.T) { 124 | var cr Credentials 125 | var ip InstalledProduct 126 | data := SUSEConnectData{SccURL: ":", Insecure: true} 127 | 128 | _, err := RequestProducts(data, cr, ip) 129 | if err == nil || !strings.Contains(err.Error(), "missing protocol scheme") { 130 | t.Fatalf("There should be a proper error: %v", err) 131 | } 132 | } 133 | 134 | func TestFaultyRequestForProduct(t *testing.T) { 135 | var cr Credentials 136 | var ip InstalledProduct 137 | data := SUSEConnectData{SccURL: "http://", Insecure: true} 138 | 139 | _, err := RequestProducts(data, cr, ip) 140 | if err == nil || !strings.HasSuffix(err.Error(), "no Host in request URL") { 141 | t.Fatalf("There should be a proper error: %v", err) 142 | } 143 | } 144 | 145 | func TestRemoteErrorWhileRequestingProducts(t *testing.T) { 146 | // We setup a fake http server that mocks a registration server. 147 | firstRequest := true 148 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 149 | // First request should return 404 to make the function request 150 | // products and return 500 in the second request 151 | if firstRequest { 152 | http.Error(w, "something bad happened", 404) 153 | } else { 154 | http.Error(w, "something bad happened", 500) 155 | } 156 | firstRequest = false 157 | })) 158 | defer ts.Close() 159 | 160 | var cr Credentials 161 | var ip InstalledProduct 162 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 163 | 164 | _, err := RequestProducts(data, cr, ip) 165 | if err == nil || err.Error() != "Unexpected error while retrieving products with regCode : 500 Internal Server Error" { 166 | t.Fatalf("It should have a proper error: %v", err) 167 | } 168 | } 169 | 170 | func TestValidRequestForProduct(t *testing.T) { 171 | // We setup a fake http server that mocks a registration server. 172 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 173 | file, err := os.Open("testdata/products-sle12.json") 174 | if err != nil { 175 | fmt.Fprintln(w, "FAIL!") 176 | return 177 | } 178 | io.Copy(w, file) 179 | file.Close() 180 | })) 181 | defer ts.Close() 182 | 183 | var cr Credentials 184 | var ip InstalledProduct 185 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 186 | 187 | products, err := RequestProducts(data, cr, ip) 188 | if err != nil { 189 | t.Fatal("It should've run just fine...") 190 | } 191 | 192 | if len(products) != 1 { 193 | t.Fatalf("Unexpected number of products found. Got %d, expected %d", len(products), 1) 194 | } 195 | 196 | productHelperSLE12(t, products[0]) 197 | } 198 | 199 | func TestValidRequestForProductUsingRMT(t *testing.T) { 200 | // We setup a fake http server that mocks a registration server. 201 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 202 | // SMT servers return 404 on this URL 203 | if r.URL.Path == "/connect/systems/subscriptions" { 204 | http.Error(w, "", http.StatusNotFound) 205 | } 206 | 207 | // The result also looks slightly different 208 | resFile := "testdata/products-sle15-rmt.json" 209 | file, err := os.Open(resFile) 210 | if err != nil { 211 | fmt.Fprintln(w, "FAIL!") 212 | return 213 | } 214 | io.Copy(w, file) 215 | file.Close() 216 | })) 217 | defer ts.Close() 218 | 219 | var cr Credentials 220 | var ip InstalledProduct 221 | data := SUSEConnectData{SccURL: ts.URL, Insecure: true} 222 | 223 | products, err := RequestProducts(data, cr, ip) 224 | if err != nil { 225 | t.Fatal("It should've run just fine...") 226 | } 227 | 228 | if len(products) != 1 { 229 | t.Fatalf("Unexpected number of products found. Got %d, expected %d", len(products), 1) 230 | } 231 | 232 | productHelperSLE15RMT(t, products[0]) 233 | } 234 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # container-suseconnect [![Golang Tests](https://github.com/SUSE/container-suseconnect/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/SUSE/container-suseconnect/actions/workflows/ci.yaml) [![GoDoc](https://godoc.org/github.com/SUSE/container-suseconnect?status.png)](https://godoc.org/github.com/SUSE/container-suseconnect) 2 | 3 | container-suseconnect provides a [ZYpp service plugin](https://doc.opensuse.org/projects/libzypp/HEAD/zypp-plugins.html#plugin-services), 4 | a [ZYpp url resolver plugin](https://doc.opensuse.org/projects/libzypp/HEAD/zypp-plugins.html#plugin-url-resolver) 5 | and command line interface. 6 | 7 | It gives access to repositories during docker build and run using the host machine credentials. 8 | 9 | ## Command line interface 10 | 11 | The application runs as ZYpp service per default if the name of the executable 12 | is `container-suseconnect-zypp`. It will run as a ZYpp URL resolver if the name 13 | of the executable is `susecloud`. In every other case it assumes that a real 14 | user executes the application. The help output of container-suseconnect shows 15 | all available commands and indicates the current defaults: 16 | 17 | ```bash 18 | container-suseconnect -h 19 | NAME: 20 | container-suseconnect - Access zypper repositories from within containers 21 | 22 | USAGE: 23 | This application can be used to retrieve basic metadata about SLES 24 | related products and module extensions. 25 | 26 | Please use the 'list-products' subcommand for listing all currently 27 | available products including their repositories and a short description. 28 | 29 | Use the 'list-modules' subcommand for listing available modules, where 30 | their 'Identifier' can be used to enable them via the ADDITIONAL_MODULES 31 | environment variable during container creation/run. When enabling multiple 32 | modules the identifiers are expected to be comma-separated. 33 | 34 | The 'zypper' subcommand runs the application as zypper plugin and is only 35 | intended to use for debugging purposes. 36 | 37 | VERSION: 38 | 2.3.0 39 | 40 | COMMANDS: 41 | list-products, lp List available products (default) 42 | list-modules, lm List available modules 43 | zypper, z, zypp Run the zypper service plugin 44 | help, h Shows a list of commands or help for one command 45 | 46 | GLOBAL OPTIONS: 47 | --help, -h show help (default: false) 48 | --version, -v print the version (default: false) 49 | 50 | COPYRIGHT: 51 | © 2020 SUSE LLC 52 | ``` 53 | 54 | ## Logging 55 | 56 | By default, this program will log everything into the 57 | `/var/log/suseconnect.log` file. Otherwise, you can optionally specify a custom 58 | path with the `SUSECONNECT_LOG_FILE` environment variable. To retrieve the 59 | contents of the log, you will have to mount the needed volume in your host. 60 | 61 | You can do this with either the 62 | [VOLUME](https://docs.docker.com/engine/reference/builder/#volume) instruction 63 | in your Dockerfile, or with the `-v` parameter when running `docker run`. Read 64 | more about this [here](https://docs.docker.com/storage/volumes/). 65 | 66 | Finally, if neither the default `/var/log/suseconnect.log` file nor the file 67 | specified through the `SUSECONNECT_LOG_FILE` environment variable are writable, 68 | then this program will log to the standard error output by default. 69 | 70 | ## Example Dockerfile 71 | 72 | Creating a SLE 15 Image 73 | 74 | The following `Dockerfile` creates a simple container image based on SLE 15: 75 | 76 | ```Dockerfile 77 | FROM registry.suse.com/suse/sle15:latest 78 | 79 | RUN zypper --gpg-auto-import-keys ref -s 80 | RUN zypper -n in vim 81 | ``` 82 | 83 | When the Docker host machine is registered against an internal SMT server, the 84 | Docker image requires the SSL certificate used by SMT: 85 | 86 | ```Dockerfile 87 | FROM registry.suse.com/suse/sle15:latest 88 | 89 | # Import the crt file of our private SMT server 90 | ADD http://smt.test.lan/smt.crt /etc/pki/trust/anchors/smt.crt 91 | RUN update-ca-certificates 92 | 93 | RUN zypper --gpg-auto-import-keys ref -s 94 | RUN zypper -n in vim 95 | ``` 96 | 97 | Creating a Custom SLE 15 SP5 Image 98 | 99 | The following `Dockerfile` creates a simple container image based on SLE 15 SP5: 100 | 101 | ```Dockerfile 102 | FROM registry.suse.com/suse/sle15:15.5 103 | 104 | RUN zypper --gpg-auto-import-keys ref -s 105 | RUN zypper -n in vim 106 | ``` 107 | 108 | After registering the host machine against an internal SMT server, the Docker 109 | image requires the SSL certificate used by SMT: 110 | 111 | ```Dockerfile 112 | FROM registry.suse.com/suse/sle15:15.5 113 | 114 | # Import the crt file of our private SMT server 115 | ADD http://smt.test.lan/smt.crt /etc/ssl/certs/smt.pem 116 | RUN c_rehash /etc/ssl/certs 117 | 118 | RUN zypper --gpg-auto-import-keys ref -s 119 | RUN zypper -n in vim 120 | ``` 121 | 122 | All recommended package modules are enabled by default. It is possible to 123 | enable additional non-recommended modules via the `identifier` by setting the 124 | environment variable `ADDITIONAL_MODULES`. When enabling multiple modules the 125 | identifiers are expected to be comma-separated: 126 | 127 | ```Dockerfile 128 | FROM registry.suse.com/suse/sle15:latest 129 | 130 | ENV ADDITIONAL_MODULES sle-module-desktop-applications,sle-module-development-tools 131 | 132 | RUN zypper --gpg-auto-import-keys ref -s 133 | RUN zypper -n in gvim 134 | ``` 135 | 136 | Examples taken from 137 | 138 | 139 | ### Building images on SLE systems registered with RMT or SMT 140 | 141 | When the host system used for building the docker images is registered against 142 | RMT or SMT it is only possible to build containers for the same SLE code base 143 | as the host system is running on. I.e. if you docker host is a SLE15 system you 144 | can only build SLE15 based images out of the box. 145 | 146 | If you want to build for a different SLE version than what is running on the 147 | host machine you will need to inject matching credentials for that target 148 | release into the build. For details on how to achieve that please follow the 149 | steps outlined in the [Building images on non SLE 150 | distributions](#building-images-on-non-sle-distributions) 151 | section. 152 | 153 | ### Building images on-demand SLE instances in the public cloud 154 | 155 | When building container images on SLE instances that were launched as so-called 156 | "on-demand" or "pay as you go" instances on a Public Cloud (as AWS, GCE or 157 | Azure) some additional steps have to be performed. 158 | 159 | For installing packages and updates the "on-demand" public cloud instance are 160 | connected to a public cloud specific update infrastructure which is based 161 | around RMT servers operated by SUSE on the various Public Cloud Providers. 162 | 163 | To be able access this update infrastructure instances need to perform 164 | additional steps to locate the required services and authenticate with them. 165 | More details on this are outlined in a 166 | [Blog Series on suse.com starting here](https://suse.com/c/a-new-update-infrastructure-for-the-public-cloud/). 167 | 168 | In order to build containers on this type of instances a new service was 169 | introduced, that service is called `containerbuild-regionsrv` and will be 170 | available in the public cloud images provided through the Marketplaces of the 171 | various Public Cloud Providers. So before building an image this service has 172 | to be started on the public cloud instance 173 | 174 | ```bash 175 | systemctl start containerbuild-regionsrv 176 | ``` 177 | 178 | In order to have it started automatically (e.g. after reboot) please use: 179 | 180 | ```bash 181 | systemctl enable containerbuild-regionsrv 182 | ``` 183 | 184 | The zypper plugins provided by `container-suseconnect` will then connect to 185 | this service for getting authentication details and information about which 186 | update server to talk to. In order for that to work the container has to be 187 | built with host networking enabled. I.e. you need to call `docker build` with 188 | `--network host`: 189 | 190 | ```bash 191 | docker build --network host 192 | ``` 193 | 194 | Since update infrastructure in the Public Clouds is based upon RMT, the same 195 | restrictions with regard to building SLE images for SLE versions differing from 196 | the SLE version of the host apply here as well. (See above) 197 | 198 | ### Building images on non SLE distributions 199 | 200 | It is possible to build SLE based docker images on other distributions as well. 201 | 202 | For that the following two files from a base SLE system are needed: 203 | 204 | - `/etc/SUSEConnect` for the API access point 205 | - `/etc/zypp/credentials.d/SCCcredentials` for providing the user credentials 206 | 207 | These files can be copied from a SLE machine that has been successfully 208 | registered at the SUSE Customer Center, RMT or SMT. 209 | 210 | A Docker version of 18.09 or above is needed to provide a secure way to mount 211 | the credentials into the image build process. 212 | 213 | A `Dockerfile` for building a SLE15 image which contains `go` would then look as follows: 214 | 215 | ```Dockerfile 216 | # syntax=docker/dockerfile:1.0.0-experimental 217 | FROM registry.suse.com/suse/sle15:latest 218 | 219 | ARG ADDITIONAL_MODULES 220 | RUN --mount=type=secret,id=SUSEConnect,required \ 221 | --mount=type=secret,id=SCCcredentials,required \ 222 | zypper -n --gpg-auto-import-keys in go 223 | ``` 224 | 225 | This file mounts all necessary secrets into the image during the build process 226 | and removes it afterwards. Please note that the first line of the `Dockerfile` 227 | is mandatory at the time of writing. Both files `SUSEConnect` and 228 | `SCCcredentials` needs to be available beside the `Dockerfile`. 229 | 230 | After creating the file you can build the image by executing: 231 | 232 | ```bash 233 | docker build -t sle15-go \ 234 | --build-arg ADDITIONAL_MODULES=sle-module-development-tools \ 235 | --secret id=SUSEConnect,src=SUSEConnect \ 236 | --secret id=SCCcredentials,src=SCCcredentials \ 237 | . 238 | ``` 239 | 240 | At the time of writing (docker 18.09.0) it is necessary to enable the Docker 241 | BuildKit by setting the environment variable `DOCKER_BUILDKIT`. The 242 | `ADDITIONAL_MODULES` are used here to enable all needed repositories. After the 243 | image has been built the package should be usable within the docker image: 244 | 245 | ```bash 246 | docker run sle15-go go version 247 | go version go1.9.7 linux/amd64 248 | ``` 249 | 250 | Please keep in mind that it is not possible to use `container-suseconnect` or 251 | `zypper` within the container after the build, because the secrets are not 252 | available any more. 253 | 254 | 255 | ### Alternative approach when using podman as the container runtime 256 | 257 | When using `podman` as the container runtime, add the following to 258 | `/etc/containers/mounts.conf` (when building containers as `root`) or 259 | `~/.config/containers/mounts.conf` (when building containers as a regular 260 | user): 261 | 262 | ```text 263 | /SUSEConnect:/etc/SUSEConnect 264 | /SCCcredentials:/etc/zypp/credentials.d/SCCcredentials 265 | ``` 266 | 267 | No further changes are needed in `Dockerfile`. 268 | 269 | ### Obtaining the SUSEConnect and SCCcredentials secrets 270 | 271 | Ideally the host system on which the container builds are executed is 272 | registered. The credentials stored on the host system in 273 | `/etc/zypp/credentials.d/SCCcredentials` 274 | can be used to register containers running or building on that host as well. 275 | 276 | Otherwise start the container that should be registered and execute 277 | `SUSEConnect` inside it: 278 | 279 | ```bash 280 | SUSEConnect -e -r 281 | ``` 282 | 283 | In case you have an unused registration code, you can connect with the username "regcode" and use the registration code as the password. 284 | 285 | For interactive use of containers you can also 286 | pass the obtained username, password and system token via environment variables: 287 | 288 | ```bash 289 | docker run -e SCC_CREDENTIAL_USERNAME= \ 290 | -e SCC_CREDENTIAL_PASSWORD= \ 291 | -e SCC_CREDENTIAL_SYSTEM_TOKEN= \ 292 | my-image 293 | ``` 294 | 295 | # License 296 | 297 | Licensed under the Apache License, Version 2.0. See 298 | [LICENSE](./LICENSE) for the full license text. 299 | -------------------------------------------------------------------------------- /internal/result.txt: -------------------------------------------------------------------------------- 1 | Name: SUSE Linux Enterprise Server 15 x86_64 2 | Type: base 3 | Identifier: SLES 4 | Based on: none 5 | Recommended: false 6 | Description: SUSE Linux Enterprise offers a comprehensive suite of products built on a single code base. The platform addresses business needs from the smallest thin-client devices to the world's most powerful high-performance computing and mainframe servers. SUSE Linux Enterprise offers common management tools and technology certifications across the platform, and each product is enterprise-class. 7 | Repositories: 8 | 1. SLE15-Installer-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-INSTALLER/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 9 | 2. SLE-Product-SLES15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 10 | 3. SLE-Product-SLES15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 11 | 4. SLE-Product-SLES15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 12 | 5. SLE-Product-SLES15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 13 | 6. SLE-Product-SLES15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 14 | 15 | Name: Basesystem Module 15 x86_64 16 | Type: module 17 | Identifier: sle-module-basesystem 18 | Based on: SLES 19 | Recommended: true 20 | Description: The SUSE Linux Enterprise Basesystem Module delivers the base system of the product. 21 | Repositories: 22 | 1. SLE-Module-Basesystem15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 23 | 2. SLE-Module-Basesystem15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 24 | 3. SLE-Module-Basesystem15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 25 | 4. SLE-Module-Basesystem15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 26 | 5. SLE-Module-Basesystem15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 27 | 28 | Name: SUSE Linux Enterprise Live Patching 15 x86_64 29 | Type: module 30 | Identifier: sle-module-live-patching 31 | Based on: sle-module-basesystem 32 | Recommended: false 33 | Description: SUSE Linux Enterprise Live Patching provides packages to update critical components in SUSE Linux Enterprise. With SUSE Linux Enterprise Live Patching, you can perform critical patching without shutting down your system, reducing the need for planned downtime and increasing service availability. 34 | Repositories: 35 | 1. SLE-Module-Live-Patching15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 36 | 2. SLE-Module-Live-Patching15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 37 | 3. SLE-Module-Live-Patching15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Live-Patching/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 38 | 4. SLE-Module-Live-Patching15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Live-Patching/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 39 | 5. SLE-Module-Live-Patching15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 40 | 41 | Name: SUSE Package Hub 15 x86_64 42 | Type: extension 43 | Identifier: PackageHub 44 | Based on: sle-module-basesystem 45 | Recommended: false 46 | Description: SUSE Package Hub is a free of charge extension providing access to community maintained packages built to run on SUSE Linux Enterprise Server. Built from the same sources used in the openSUSE distributions, these quality packages provide additional software to what is found in the SUSE Linux Enterprise Server product. Packages from the Package Hub are delivered without L3 support from SUSE. General updates and fixes to the packages in SUSE Package Hub are provided by the openSUSE community based on their discretion though SUSE will monitor and ensure that any known critical security issues will be addressed. Packages in the Package Hub are approved by SUSE for use with SUSE Linux Enterprise Server 15 and to not interfere with the supportability of SUSE Linux Enterprise Server it's modules and extensions. For more information about SUSE Package Hub please visit https://packagehub.suse.com. 47 | Repositories: 48 | 1. SUSE-PackageHub-15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 49 | 2. SUSE-PackageHub-15-Debuginfo: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/standard_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 50 | 3. SUSE-PackageHub-15-Standard-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/standard/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 51 | 4. SLE-Module-Packagehub-Subpackages15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 52 | 5. SLE-Module-Packagehub-Subpackages15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 53 | 6. SLE-Module-Packagehub-Subpackages15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 54 | 7. SLE-Module-Packagehub-Subpackages15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Packagehub-Subpackages/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 55 | 8. SLE-Module-Packagehub-Subpackages15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Packagehub-Subpackages/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 56 | 57 | Name: Containers Module 15 x86_64 58 | Type: module 59 | Identifier: sle-module-containers 60 | Based on: sle-module-basesystem 61 | Recommended: false 62 | Description: This Module contains several packages revolving around containers and related tools. 63 | Repositories: 64 | 1. SLE-Module-Containers15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 65 | 2. SLE-Module-Containers15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 66 | 3. SLE-Module-Containers15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 67 | 4. SLE-Module-Containers15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Containers/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 68 | 5. SLE-Module-Containers15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Containers/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 69 | 70 | Name: Server Applications Module 15 x86_64 71 | Type: module 72 | Identifier: sle-module-server-applications 73 | Based on: sle-module-basesystem 74 | Recommended: true 75 | Description: The SUSE Linux Enterprise Server Applications Module delivers a basic set of Server functionality. Access to the Server Applications Module is included in your SUSE Linux Enterprise Server subscription. 76 | Repositories: 77 | 1. SLE-Module-Server-Applications15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 78 | 2. SLE-Module-Server-Applications15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 79 | 3. SLE-Module-Server-Applications15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 80 | 4. SLE-Module-Server-Applications15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 81 | 5. SLE-Module-Server-Applications15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 82 | 83 | Name: Web and Scripting Module 15 x86_64 84 | Type: module 85 | Identifier: sle-module-web-scripting 86 | Based on: sle-module-server-applications 87 | Recommended: false 88 | Description: The SUSE Linux Enterprise Web and Scripting Module should contains additional packages that are helpful when running a webserver. Access to the Web and Scripting Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself: Package versions in this module are usually supported for at most three years. We are planning to release more recent versions on a schedule of approximately 18 month; the exact dates may differ per package. 89 | Repositories: 90 | 1. SLE-Module-Web-Scripting15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 91 | 2. SLE-Module-Web-Scripting15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 92 | 3. SLE-Module-Web-Scripting15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 93 | 4. SLE-Module-Web-Scripting15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Web-Scripting/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 94 | 5. SLE-Module-Web-Scripting15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Web-Scripting/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 95 | 96 | Name: Legacy Module 15 x86_64 97 | Type: module 98 | Identifier: sle-module-legacy 99 | Based on: sle-module-server-applications 100 | Recommended: false 101 | Description: The Legacy Module helps you migrating applications from SUSE Linux Enterprise 12 and other systems to SUSE Linux Enterprise 15, by providing packages which are discontinued on SUSE Linux Enterprise Server, such as: ntp, IBM Java 8, and a number of libraries. Access to the Legacy Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself. Packages in the this module are usually supported for at most three years. 102 | Repositories: 103 | 1. SLE-Module-Legacy15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Legacy/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 104 | 2. SLE-Module-Legacy15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Legacy/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 105 | 3. SLE-Module-Legacy15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 106 | 4. SLE-Module-Legacy15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 107 | 5. SLE-Module-Legacy15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 108 | 109 | Name: Public Cloud Module 15 x86_64 110 | Type: module 111 | Identifier: sle-module-public-cloud 112 | Based on: sle-module-server-applications 113 | Recommended: false 114 | Description: The Public Cloud Module is a collection of tools that enables you to create and manage cloud images from the commandline on SUSE Linux Enterprise Server. When building your own images with KIWI or SUSE Studio, initialization code specific to the target cloud is included in that image. Access to the Public Cloud Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself; please check the Release Notes for further details. 115 | Repositories: 116 | 1. SLE-Module-Public-Cloud15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 117 | 2. SLE-Module-Public-Cloud15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 118 | 3. SLE-Module-Public-Cloud15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Public-Cloud/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 119 | 4. SLE-Module-Public-Cloud15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Public-Cloud/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 120 | 5. SLE-Module-Public-Cloud15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 121 | 122 | Name: SUSE Linux Enterprise High Availability Extension 15 x86_64 123 | Type: extension 124 | Identifier: sle-ha 125 | Based on: sle-module-server-applications 126 | Recommended: false 127 | Description: SUSE Linux High Availability Extension provides mature, industry-leading open-source high-availability clustering technologies that are easy to set up and use. It can be deployed in physical and/or virtual environments, and can cluster physical servers, virtual servers, or any combination of the two to suit your business’ needs. 128 | Repositories: 129 | 1. SLE-Product-HA15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 130 | 2. SLE-Product-HA15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 131 | 3. SLE-Product-HA15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 132 | 4. SLE-Product-HA15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-HA/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 133 | 5. SLE-Product-HA15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-HA/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 134 | 135 | Name: Desktop Applications Module 15 x86_64 136 | Type: module 137 | Identifier: sle-module-desktop-applications 138 | Based on: sle-module-basesystem 139 | Recommended: false 140 | Description: The SUSE Linux Enterprise Desktop Applications Module delivers a basic set of Desktop functionality. Access to the Desktop Applications Module is included in your SUSE Linux Enterprise product subscription. 141 | Repositories: 142 | 1. SLE-Module-Desktop-Applications15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 143 | 2. SLE-Module-Desktop-Applications15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 144 | 3. SLE-Module-Desktop-Applications15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 145 | 4. SLE-Module-Desktop-Applications15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 146 | 5. SLE-Module-Desktop-Applications15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 147 | 148 | Name: Development Tools Module 15 x86_64 149 | Type: module 150 | Identifier: sle-module-development-tools 151 | Based on: sle-module-desktop-applications 152 | Recommended: false 153 | Description: The Development Tools Module helps you developing applications for SUSE Linux Enterprise 15. Access to the Development Tools Module is included in your SUSE Linux Enterprise product subscription. The module has a different lifecycle than SUSE Linux Enterprise itself. 154 | Repositories: 155 | 1. SLE-Module-DevTools15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 156 | 2. SLE-Module-DevTools15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 157 | 3. SLE-Module-DevTools15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 158 | 4. SLE-Module-DevTools15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 159 | 5. SLE-Module-DevTools15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 160 | 161 | Name: SUSE Linux Enterprise Workstation Extension 15 x86_64 162 | Type: extension 163 | Identifier: sle-we 164 | Based on: sle-module-desktop-applications 165 | Recommended: false 166 | Description: SUSE Linux Enterprise Workstation Extension adds additional functionality to a base SUSE Linux Enterprise installation. The Workstation Extension offers additional desktop applications (office suite, email client, graphical editor, multimedia tools) and libraries. Workstation Extension is enabled and installed by default on SUSE Linux Enterprise Desktop installation. Adding the Workstation Extension to a SUSE Linux Enterprise Server installation allows to seamlessly combine both products to create a full featured server workstation. 167 | Repositories: 168 | 1. SLE-15-GA-Desktop-NVIDIA-Driver: http://smt-ec2.susecloud.net/repo/RPMMD/SLE-15-GA-Desktop-NVIDIA-Driver?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 169 | 2. SLE-Product-WE15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 170 | 3. SLE-Product-WE15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 171 | 4. SLE-Product-WE15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 172 | 5. SLE-Product-WE15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-WE/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 173 | 6. SLE-Product-WE15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-WE/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 174 | 175 | Name: SUSE Cloud Application Platform Tools Module 15 x86_64 176 | Type: module 177 | Identifier: sle-module-cap-tools 178 | Based on: sle-module-basesystem 179 | Recommended: false 180 | Description: The SUSE Cloud Application Platform Tools Module is a collection of tools that enables you to interact with the SUSE Cloud Application Platform product itself, providing the commandline client for instance. Access to the SUSE Cloud Application Platform Tools Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself; please check the Release Notes for further details. 181 | Repositories: 182 | 1. SLE-Module-CAP-Tools15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 183 | 2. SLE-Module-CAP-Tools15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 184 | 3. SLE-Module-CAP-Tools15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-CAP-Tools/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 185 | 4. SLE-Module-CAP-Tools15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-CAP-Tools/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 186 | 5. SLE-Module-CAP-Tools15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 187 | 188 | -------------------------------------------------------------------------------- /internal/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package containersuseconnect 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "log" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func testServiceOutput( 26 | t *testing.T, 27 | filePath string, 28 | expectedOutput string, 29 | dumpFunction func(*bytes.Buffer, []Product), 30 | ) { 31 | reader, err := os.Open(filePath) 32 | if err != nil { 33 | t.Fatal("Could not read JSON file...") 34 | } 35 | defer reader.Close() 36 | 37 | products, err := parseProducts(reader) 38 | if err != nil { 39 | t.Error(err.Error()) 40 | } 41 | if len(products) != 1 { 42 | t.Fatalf("Unexpected number of products found. Got %d, expected %d", len(products), 1) 43 | } 44 | 45 | buf := bytes.Buffer{} 46 | dumpFunction(&buf, products) 47 | 48 | result := buf.String() 49 | if result != expectedOutput { 50 | t.Error(result) 51 | } 52 | 53 | file, err := os.Create("result.txt") 54 | if err != nil { 55 | log.Fatal("Cannot create file", err) 56 | } 57 | defer file.Close() 58 | 59 | fmt.Fprint(file, result) 60 | } 61 | 62 | func TestServiceOutputSLE12(t *testing.T) { 63 | const expectedOutput = `# generated by container-suseconnect 64 | 65 | [SLES12-Updates] 66 | name=SLES12-Updates for sle-12-x86_64 67 | baseurl=https://smt.test.lan/repo/SUSE/Updates/SLE-SERVER/12/x86_64/update 68 | autorefresh=1 69 | enabled=1 70 | 71 | [SLES12-Debuginfo-Updates] 72 | name=SLES12-Debuginfo-Updates for sle-12-x86_64 73 | baseurl=https://smt.test.lan/repo/SUSE/Updates/SLE-SERVER/12/x86_64/update_debug 74 | autorefresh=1 75 | enabled=0 76 | 77 | [SLES12-Pool] 78 | name=SLES12-Pool for sle-12-x86_64 79 | baseurl=https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product 80 | autorefresh=0 81 | enabled=1 82 | 83 | [SLES12-Debuginfo-Pool] 84 | name=SLES12-Debuginfo-Pool for sle-12-x86_64 85 | baseurl=https://smt.test.lan/repo/SUSE/Products/SLE-SERVER/12/x86_64/product_debug 86 | autorefresh=0 87 | enabled=0 88 | 89 | ` 90 | testServiceOutput(t, "testdata/products-sle12.json", expectedOutput, 91 | func(buffer *bytes.Buffer, products []Product) { 92 | DumpRepositories(buffer, products[0]) 93 | }) 94 | } 95 | 96 | func TestServiceOutputSLE15(t *testing.T) { 97 | const expectedOutput = `# generated by container-suseconnect 98 | 99 | [SLE15-Installer-Updates] 100 | name=SLE15-Installer-Updates for sle-15-x86_64 101 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-INSTALLER/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 102 | autorefresh=1 103 | enabled=0 104 | 105 | [SLE-Product-SLES15-Pool] 106 | name=SLE-Product-SLES15-Pool for sle-15-x86_64 107 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 108 | autorefresh=0 109 | enabled=1 110 | 111 | [SLE-Product-SLES15-Updates] 112 | name=SLE-Product-SLES15-Updates for sle-15-x86_64 113 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 114 | autorefresh=1 115 | enabled=1 116 | 117 | [SLE-Product-SLES15-Debuginfo-Updates] 118 | name=SLE-Product-SLES15-Debuginfo-Updates for sle-15-x86_64 119 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 120 | autorefresh=1 121 | enabled=0 122 | 123 | [SLE-Product-SLES15-Source-Pool] 124 | name=SLE-Product-SLES15-Source-Pool for sle-15-x86_64 125 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 126 | autorefresh=0 127 | enabled=0 128 | 129 | [SLE-Product-SLES15-Debuginfo-Pool] 130 | name=SLE-Product-SLES15-Debuginfo-Pool for sle-15-x86_64 131 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 132 | autorefresh=0 133 | enabled=0 134 | 135 | [SLE-Module-Basesystem15-Updates] 136 | name=SLE-Module-Basesystem15-Updates for sle-15-x86_64 137 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 138 | autorefresh=1 139 | enabled=1 140 | 141 | [SLE-Module-Basesystem15-Source-Pool] 142 | name=SLE-Module-Basesystem15-Source-Pool for sle-15-x86_64 143 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 144 | autorefresh=0 145 | enabled=0 146 | 147 | [SLE-Module-Basesystem15-Debuginfo-Updates] 148 | name=SLE-Module-Basesystem15-Debuginfo-Updates for sle-15-x86_64 149 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 150 | autorefresh=1 151 | enabled=0 152 | 153 | [SLE-Module-Basesystem15-Pool] 154 | name=SLE-Module-Basesystem15-Pool for sle-15-x86_64 155 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 156 | autorefresh=0 157 | enabled=1 158 | 159 | [SLE-Module-Basesystem15-Debuginfo-Pool] 160 | name=SLE-Module-Basesystem15-Debuginfo-Pool for sle-15-x86_64 161 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 162 | autorefresh=0 163 | enabled=0 164 | 165 | [SLE-Module-Server-Applications15-Updates] 166 | name=SLE-Module-Server-Applications15-Updates for sle-15-x86_64 167 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 168 | autorefresh=1 169 | enabled=1 170 | 171 | [SLE-Module-Server-Applications15-Debuginfo-Updates] 172 | name=SLE-Module-Server-Applications15-Debuginfo-Updates for sle-15-x86_64 173 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 174 | autorefresh=1 175 | enabled=0 176 | 177 | [SLE-Module-Server-Applications15-Pool] 178 | name=SLE-Module-Server-Applications15-Pool for sle-15-x86_64 179 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 180 | autorefresh=0 181 | enabled=1 182 | 183 | [SLE-Module-Server-Applications15-Debuginfo-Pool] 184 | name=SLE-Module-Server-Applications15-Debuginfo-Pool for sle-15-x86_64 185 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 186 | autorefresh=0 187 | enabled=0 188 | 189 | [SLE-Module-Server-Applications15-Source-Pool] 190 | name=SLE-Module-Server-Applications15-Source-Pool for sle-15-x86_64 191 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 192 | autorefresh=0 193 | enabled=0 194 | 195 | ` 196 | 197 | testServiceOutput(t, "testdata/products-sle15.json", expectedOutput, 198 | func(buffer *bytes.Buffer, products []Product) { 199 | DumpRepositories(buffer, products[0]) 200 | }) 201 | } 202 | 203 | func TestServiceOutputSLE15WithCustomModules(t *testing.T) { 204 | os.Setenv( 205 | "ADDITIONAL_MODULES", 206 | "sle-module-desktop-applications,sle-module-development-tools", 207 | ) 208 | const expectedOutput = `# generated by container-suseconnect 209 | 210 | [SLE15-Installer-Updates] 211 | name=SLE15-Installer-Updates for sle-15-x86_64 212 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-INSTALLER/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 213 | autorefresh=1 214 | enabled=0 215 | 216 | [SLE-Product-SLES15-Pool] 217 | name=SLE-Product-SLES15-Pool for sle-15-x86_64 218 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 219 | autorefresh=0 220 | enabled=1 221 | 222 | [SLE-Product-SLES15-Updates] 223 | name=SLE-Product-SLES15-Updates for sle-15-x86_64 224 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 225 | autorefresh=1 226 | enabled=1 227 | 228 | [SLE-Product-SLES15-Debuginfo-Updates] 229 | name=SLE-Product-SLES15-Debuginfo-Updates for sle-15-x86_64 230 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 231 | autorefresh=1 232 | enabled=0 233 | 234 | [SLE-Product-SLES15-Source-Pool] 235 | name=SLE-Product-SLES15-Source-Pool for sle-15-x86_64 236 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 237 | autorefresh=0 238 | enabled=0 239 | 240 | [SLE-Product-SLES15-Debuginfo-Pool] 241 | name=SLE-Product-SLES15-Debuginfo-Pool for sle-15-x86_64 242 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 243 | autorefresh=0 244 | enabled=0 245 | 246 | [SLE-Module-Basesystem15-Updates] 247 | name=SLE-Module-Basesystem15-Updates for sle-15-x86_64 248 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 249 | autorefresh=1 250 | enabled=1 251 | 252 | [SLE-Module-Basesystem15-Source-Pool] 253 | name=SLE-Module-Basesystem15-Source-Pool for sle-15-x86_64 254 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 255 | autorefresh=0 256 | enabled=0 257 | 258 | [SLE-Module-Basesystem15-Debuginfo-Updates] 259 | name=SLE-Module-Basesystem15-Debuginfo-Updates for sle-15-x86_64 260 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 261 | autorefresh=1 262 | enabled=0 263 | 264 | [SLE-Module-Basesystem15-Pool] 265 | name=SLE-Module-Basesystem15-Pool for sle-15-x86_64 266 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 267 | autorefresh=0 268 | enabled=1 269 | 270 | [SLE-Module-Basesystem15-Debuginfo-Pool] 271 | name=SLE-Module-Basesystem15-Debuginfo-Pool for sle-15-x86_64 272 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 273 | autorefresh=0 274 | enabled=0 275 | 276 | [SLE-Module-Server-Applications15-Updates] 277 | name=SLE-Module-Server-Applications15-Updates for sle-15-x86_64 278 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 279 | autorefresh=1 280 | enabled=1 281 | 282 | [SLE-Module-Server-Applications15-Debuginfo-Updates] 283 | name=SLE-Module-Server-Applications15-Debuginfo-Updates for sle-15-x86_64 284 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 285 | autorefresh=1 286 | enabled=0 287 | 288 | [SLE-Module-Server-Applications15-Pool] 289 | name=SLE-Module-Server-Applications15-Pool for sle-15-x86_64 290 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 291 | autorefresh=0 292 | enabled=1 293 | 294 | [SLE-Module-Server-Applications15-Debuginfo-Pool] 295 | name=SLE-Module-Server-Applications15-Debuginfo-Pool for sle-15-x86_64 296 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 297 | autorefresh=0 298 | enabled=0 299 | 300 | [SLE-Module-Server-Applications15-Source-Pool] 301 | name=SLE-Module-Server-Applications15-Source-Pool for sle-15-x86_64 302 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 303 | autorefresh=0 304 | enabled=0 305 | 306 | [SLE-Module-Desktop-Applications15-Updates] 307 | name=SLE-Module-Desktop-Applications15-Updates for sle-15-x86_64 308 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 309 | autorefresh=1 310 | enabled=1 311 | 312 | [SLE-Module-Desktop-Applications15-Debuginfo-Updates] 313 | name=SLE-Module-Desktop-Applications15-Debuginfo-Updates for sle-15-x86_64 314 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 315 | autorefresh=1 316 | enabled=0 317 | 318 | [SLE-Module-Desktop-Applications15-Pool] 319 | name=SLE-Module-Desktop-Applications15-Pool for sle-15-x86_64 320 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 321 | autorefresh=0 322 | enabled=1 323 | 324 | [SLE-Module-Desktop-Applications15-Debuginfo-Pool] 325 | name=SLE-Module-Desktop-Applications15-Debuginfo-Pool for sle-15-x86_64 326 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 327 | autorefresh=0 328 | enabled=0 329 | 330 | [SLE-Module-Desktop-Applications15-Source-Pool] 331 | name=SLE-Module-Desktop-Applications15-Source-Pool for sle-15-x86_64 332 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 333 | autorefresh=0 334 | enabled=0 335 | 336 | [SLE-Module-DevTools15-Updates] 337 | name=SLE-Module-DevTools15-Updates for sle-15-x86_64 338 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net 339 | autorefresh=1 340 | enabled=1 341 | 342 | [SLE-Module-DevTools15-Debuginfo-Updates] 343 | name=SLE-Module-DevTools15-Debuginfo-Updates for sle-15-x86_64 344 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net 345 | autorefresh=1 346 | enabled=0 347 | 348 | [SLE-Module-DevTools15-Pool] 349 | name=SLE-Module-DevTools15-Pool for sle-15-x86_64 350 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net 351 | autorefresh=0 352 | enabled=1 353 | 354 | [SLE-Module-DevTools15-Debuginfo-Pool] 355 | name=SLE-Module-DevTools15-Debuginfo-Pool for sle-15-x86_64 356 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net 357 | autorefresh=0 358 | enabled=0 359 | 360 | [SLE-Module-DevTools15-Source-Pool] 361 | name=SLE-Module-DevTools15-Source-Pool for sle-15-x86_64 362 | baseurl=http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net 363 | autorefresh=0 364 | enabled=0 365 | 366 | ` 367 | 368 | testServiceOutput(t, "testdata/products-sle15.json", expectedOutput, 369 | func(buffer *bytes.Buffer, products []Product) { 370 | DumpRepositories(buffer, products[0]) 371 | }) 372 | } 373 | 374 | func TestServiceListModulesWithSLE15(t *testing.T) { 375 | const expectedOutput = `Name: Basesystem Module 15 x86_64 376 | Identifier: sle-module-basesystem 377 | Recommended: true 378 | 379 | Name: SUSE Linux Enterprise Live Patching 15 x86_64 380 | Identifier: sle-module-live-patching 381 | Recommended: false 382 | 383 | Name: Containers Module 15 x86_64 384 | Identifier: sle-module-containers 385 | Recommended: false 386 | 387 | Name: Server Applications Module 15 x86_64 388 | Identifier: sle-module-server-applications 389 | Recommended: true 390 | 391 | Name: Web and Scripting Module 15 x86_64 392 | Identifier: sle-module-web-scripting 393 | Recommended: false 394 | 395 | Name: Legacy Module 15 x86_64 396 | Identifier: sle-module-legacy 397 | Recommended: false 398 | 399 | Name: Public Cloud Module 15 x86_64 400 | Identifier: sle-module-public-cloud 401 | Recommended: false 402 | 403 | Name: Desktop Applications Module 15 x86_64 404 | Identifier: sle-module-desktop-applications 405 | Recommended: false 406 | 407 | Name: Development Tools Module 15 x86_64 408 | Identifier: sle-module-development-tools 409 | Recommended: false 410 | 411 | Name: SUSE Cloud Application Platform Tools Module 15 x86_64 412 | Identifier: sle-module-cap-tools 413 | Recommended: false 414 | 415 | ` 416 | 417 | testServiceOutput(t, "testdata/products-sle15.json", expectedOutput, 418 | func(buffer *bytes.Buffer, products []Product) { 419 | ListModules(buffer, products) 420 | }) 421 | } 422 | 423 | func TestServiceListProductsWithSLE15(t *testing.T) { 424 | const expectedOutput = `Name: SUSE Linux Enterprise Server 15 x86_64 425 | Type: base 426 | Identifier: SLES 427 | Based on: none 428 | Recommended: false 429 | Description: SUSE Linux Enterprise offers a comprehensive suite of products built on a single code base. The platform addresses business needs from the smallest thin-client devices to the world's most powerful high-performance computing and mainframe servers. SUSE Linux Enterprise offers common management tools and technology certifications across the platform, and each product is enterprise-class. 430 | Repositories: 431 | 1. SLE15-Installer-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-INSTALLER/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 432 | 2. SLE-Product-SLES15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 433 | 3. SLE-Product-SLES15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 434 | 4. SLE-Product-SLES15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-SLES/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 435 | 5. SLE-Product-SLES15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 436 | 6. SLE-Product-SLES15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-SLES/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 437 | 438 | Name: Basesystem Module 15 x86_64 439 | Type: module 440 | Identifier: sle-module-basesystem 441 | Based on: SLES 442 | Recommended: true 443 | Description: The SUSE Linux Enterprise Basesystem Module delivers the base system of the product. 444 | Repositories: 445 | 1. SLE-Module-Basesystem15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 446 | 2. SLE-Module-Basesystem15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 447 | 3. SLE-Module-Basesystem15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Basesystem/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 448 | 4. SLE-Module-Basesystem15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 449 | 5. SLE-Module-Basesystem15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Basesystem/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 450 | 451 | Name: SUSE Linux Enterprise Live Patching 15 x86_64 452 | Type: module 453 | Identifier: sle-module-live-patching 454 | Based on: sle-module-basesystem 455 | Recommended: false 456 | Description: SUSE Linux Enterprise Live Patching provides packages to update critical components in SUSE Linux Enterprise. With SUSE Linux Enterprise Live Patching, you can perform critical patching without shutting down your system, reducing the need for planned downtime and increasing service availability. 457 | Repositories: 458 | 1. SLE-Module-Live-Patching15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 459 | 2. SLE-Module-Live-Patching15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 460 | 3. SLE-Module-Live-Patching15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Live-Patching/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 461 | 4. SLE-Module-Live-Patching15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Live-Patching/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 462 | 5. SLE-Module-Live-Patching15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Live-Patching/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 463 | 464 | Name: SUSE Package Hub 15 x86_64 465 | Type: extension 466 | Identifier: PackageHub 467 | Based on: sle-module-basesystem 468 | Recommended: false 469 | Description: SUSE Package Hub is a free of charge extension providing access to community maintained packages built to run on SUSE Linux Enterprise Server. Built from the same sources used in the openSUSE distributions, these quality packages provide additional software to what is found in the SUSE Linux Enterprise Server product. Packages from the Package Hub are delivered without L3 support from SUSE. General updates and fixes to the packages in SUSE Package Hub are provided by the openSUSE community based on their discretion though SUSE will monitor and ensure that any known critical security issues will be addressed. Packages in the Package Hub are approved by SUSE for use with SUSE Linux Enterprise Server 15 and to not interfere with the supportability of SUSE Linux Enterprise Server it's modules and extensions. For more information about SUSE Package Hub please visit https://packagehub.suse.com. 470 | Repositories: 471 | 1. SUSE-PackageHub-15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 472 | 2. SUSE-PackageHub-15-Debuginfo: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/standard_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 473 | 3. SUSE-PackageHub-15-Standard-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Backports/SLE-15_x86_64/standard/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 474 | 4. SLE-Module-Packagehub-Subpackages15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 475 | 5. SLE-Module-Packagehub-Subpackages15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 476 | 6. SLE-Module-Packagehub-Subpackages15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Packagehub-Subpackages/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 477 | 7. SLE-Module-Packagehub-Subpackages15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Packagehub-Subpackages/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 478 | 8. SLE-Module-Packagehub-Subpackages15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Packagehub-Subpackages/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 479 | 480 | Name: Containers Module 15 x86_64 481 | Type: module 482 | Identifier: sle-module-containers 483 | Based on: sle-module-basesystem 484 | Recommended: false 485 | Description: This Module contains several packages revolving around containers and related tools. 486 | Repositories: 487 | 1. SLE-Module-Containers15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 488 | 2. SLE-Module-Containers15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 489 | 3. SLE-Module-Containers15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Containers/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 490 | 4. SLE-Module-Containers15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Containers/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 491 | 5. SLE-Module-Containers15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Containers/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 492 | 493 | Name: Server Applications Module 15 x86_64 494 | Type: module 495 | Identifier: sle-module-server-applications 496 | Based on: sle-module-basesystem 497 | Recommended: true 498 | Description: The SUSE Linux Enterprise Server Applications Module delivers a basic set of Server functionality. Access to the Server Applications Module is included in your SUSE Linux Enterprise Server subscription. 499 | Repositories: 500 | 1. SLE-Module-Server-Applications15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 501 | 2. SLE-Module-Server-Applications15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Server-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 502 | 3. SLE-Module-Server-Applications15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 503 | 4. SLE-Module-Server-Applications15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 504 | 5. SLE-Module-Server-Applications15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Server-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 505 | 506 | Name: Web and Scripting Module 15 x86_64 507 | Type: module 508 | Identifier: sle-module-web-scripting 509 | Based on: sle-module-server-applications 510 | Recommended: false 511 | Description: The SUSE Linux Enterprise Web and Scripting Module should contains additional packages that are helpful when running a webserver. Access to the Web and Scripting Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself: Package versions in this module are usually supported for at most three years. We are planning to release more recent versions on a schedule of approximately 18 month; the exact dates may differ per package. 512 | Repositories: 513 | 1. SLE-Module-Web-Scripting15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 514 | 2. SLE-Module-Web-Scripting15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 515 | 3. SLE-Module-Web-Scripting15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Web-Scripting/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 516 | 4. SLE-Module-Web-Scripting15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Web-Scripting/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 517 | 5. SLE-Module-Web-Scripting15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Web-Scripting/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 518 | 519 | Name: Legacy Module 15 x86_64 520 | Type: module 521 | Identifier: sle-module-legacy 522 | Based on: sle-module-server-applications 523 | Recommended: false 524 | Description: The Legacy Module helps you migrating applications from SUSE Linux Enterprise 12 and other systems to SUSE Linux Enterprise 15, by providing packages which are discontinued on SUSE Linux Enterprise Server, such as: ntp, IBM Java 8, and a number of libraries. Access to the Legacy Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself. Packages in the this module are usually supported for at most three years. 525 | Repositories: 526 | 1. SLE-Module-Legacy15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Legacy/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 527 | 2. SLE-Module-Legacy15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Legacy/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 528 | 3. SLE-Module-Legacy15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 529 | 4. SLE-Module-Legacy15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 530 | 5. SLE-Module-Legacy15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Legacy/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 531 | 532 | Name: Public Cloud Module 15 x86_64 533 | Type: module 534 | Identifier: sle-module-public-cloud 535 | Based on: sle-module-server-applications 536 | Recommended: false 537 | Description: The Public Cloud Module is a collection of tools that enables you to create and manage cloud images from the commandline on SUSE Linux Enterprise Server. When building your own images with KIWI or SUSE Studio, initialization code specific to the target cloud is included in that image. Access to the Public Cloud Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself; please check the Release Notes for further details. 538 | Repositories: 539 | 1. SLE-Module-Public-Cloud15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 540 | 2. SLE-Module-Public-Cloud15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 541 | 3. SLE-Module-Public-Cloud15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Public-Cloud/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 542 | 4. SLE-Module-Public-Cloud15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Public-Cloud/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 543 | 5. SLE-Module-Public-Cloud15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Public-Cloud/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 544 | 545 | Name: SUSE Linux Enterprise High Availability Extension 15 x86_64 546 | Type: extension 547 | Identifier: sle-ha 548 | Based on: sle-module-server-applications 549 | Recommended: false 550 | Description: SUSE Linux High Availability Extension provides mature, industry-leading open-source high-availability clustering technologies that are easy to set up and use. It can be deployed in physical and/or virtual environments, and can cluster physical servers, virtual servers, or any combination of the two to suit your business’ needs. 551 | Repositories: 552 | 1. SLE-Product-HA15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 553 | 2. SLE-Product-HA15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 554 | 3. SLE-Product-HA15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-HA/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 555 | 4. SLE-Product-HA15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-HA/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 556 | 5. SLE-Product-HA15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-HA/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 557 | 558 | Name: Desktop Applications Module 15 x86_64 559 | Type: module 560 | Identifier: sle-module-desktop-applications 561 | Based on: sle-module-basesystem 562 | Recommended: false 563 | Description: The SUSE Linux Enterprise Desktop Applications Module delivers a basic set of Desktop functionality. Access to the Desktop Applications Module is included in your SUSE Linux Enterprise product subscription. 564 | Repositories: 565 | 1. SLE-Module-Desktop-Applications15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 566 | 2. SLE-Module-Desktop-Applications15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Desktop-Applications/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 567 | 3. SLE-Module-Desktop-Applications15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 568 | 4. SLE-Module-Desktop-Applications15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 569 | 5. SLE-Module-Desktop-Applications15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Desktop-Applications/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 570 | 571 | Name: Development Tools Module 15 x86_64 572 | Type: module 573 | Identifier: sle-module-development-tools 574 | Based on: sle-module-desktop-applications 575 | Recommended: false 576 | Description: The Development Tools Module helps you developing applications for SUSE Linux Enterprise 15. Access to the Development Tools Module is included in your SUSE Linux Enterprise product subscription. The module has a different lifecycle than SUSE Linux Enterprise itself. 577 | Repositories: 578 | 1. SLE-Module-DevTools15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 579 | 2. SLE-Module-DevTools15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-Development-Tools/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 580 | 3. SLE-Module-DevTools15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 581 | 4. SLE-Module-DevTools15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 582 | 5. SLE-Module-DevTools15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-Development-Tools/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 583 | 584 | Name: SUSE Linux Enterprise Workstation Extension 15 x86_64 585 | Type: extension 586 | Identifier: sle-we 587 | Based on: sle-module-desktop-applications 588 | Recommended: false 589 | Description: SUSE Linux Enterprise Workstation Extension adds additional functionality to a base SUSE Linux Enterprise installation. The Workstation Extension offers additional desktop applications (office suite, email client, graphical editor, multimedia tools) and libraries. Workstation Extension is enabled and installed by default on SUSE Linux Enterprise Desktop installation. Adding the Workstation Extension to a SUSE Linux Enterprise Server installation allows to seamlessly combine both products to create a full featured server workstation. 590 | Repositories: 591 | 1. SLE-15-GA-Desktop-NVIDIA-Driver: http://smt-ec2.susecloud.net/repo/RPMMD/SLE-15-GA-Desktop-NVIDIA-Driver?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 592 | 2. SLE-Product-WE15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 593 | 3. SLE-Product-WE15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 594 | 4. SLE-Product-WE15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Product-WE/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 595 | 5. SLE-Product-WE15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-WE/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 596 | 6. SLE-Product-WE15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Product-WE/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 597 | 598 | Name: SUSE Cloud Application Platform Tools Module 15 x86_64 599 | Type: module 600 | Identifier: sle-module-cap-tools 601 | Based on: sle-module-basesystem 602 | Recommended: false 603 | Description: The SUSE Cloud Application Platform Tools Module is a collection of tools that enables you to interact with the SUSE Cloud Application Platform product itself, providing the commandline client for instance. Access to the SUSE Cloud Application Platform Tools Module is included in your SUSE Linux Enterprise Server subscription. The module has a different lifecycle than SUSE Linux Enterprise Server itself; please check the Release Notes for further details. 604 | Repositories: 605 | 1. SLE-Module-CAP-Tools15-Debuginfo-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 606 | 2. SLE-Module-CAP-Tools15-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 607 | 3. SLE-Module-CAP-Tools15-Debuginfo-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-CAP-Tools/15/x86_64/update_debug/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 608 | 4. SLE-Module-CAP-Tools15-Updates: http://smt-ec2.susecloud.net/repo/SUSE/Updates/SLE-Module-CAP-Tools/15/x86_64/update/?credentials=SMT-http_smt-ec2_susecloud_net (enabled) 609 | 5. SLE-Module-CAP-Tools15-Source-Pool: http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-Module-CAP-Tools/15/x86_64/product_source/?credentials=SMT-http_smt-ec2_susecloud_net (disabled) 610 | 611 | ` 612 | 613 | testServiceOutput(t, "testdata/products-sle15.json", expectedOutput, 614 | func(buffer *bytes.Buffer, products []Product) { 615 | ListProducts(buffer, products, "none") 616 | }) 617 | } 618 | --------------------------------------------------------------------------------