Skip to content

Commit 4da52c7

Browse files
committed
fix: add apigee support back in.
1 parent 95450b0 commit 4da52c7

10 files changed

Lines changed: 180 additions & 57 deletions

File tree

README.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ software order.
1010
You can use this CLI both as a tool and as an example of how to use Golang to
1111
call the
1212
[SAS Viya Orders API](https://developer.sas.com/rest-apis/mysas).
13-
You can also import the _assetreqs_ and _authn_ packages and use them in your own
14-
Golang project.
13+
You can also import the _assetreqs_ package and use it in your own Golang project.
1514

1615
```
1716
Usage:
@@ -48,6 +47,8 @@ Use "viya4-orders-cli [command] --help" for more information about a command.
4847
- API credentials for the
4948
[SAS Viya Orders API](https://developer.sas.com/rest-apis/mysas)
5049
are required. You can obtain them [here](https://developer.sas.com/rest-apis/mysas/applications).
50+
> [!IMPORTANT]
51+
> Above is a new developer portal that generates keys used with the new APIM proxy for Viya Orders API. If you have keys generated in the [SAS Apigee Developer Portal] (https://api.apiportal.sas.com), they will work until April 20, 2026.
5152
- [Go](https://golang.org/) 1.25.6 (or later) or [Docker](https://www.docker.com/) is required if you intend to build your own image.
5253
- [git](https://git-scm.com/) version 2 or later is required if you intend to clone the repository.
5354

@@ -118,9 +119,11 @@ Take the following steps to start using SAS Viya Orders CLI:
118119
[SAS Viya Orders API](https://developer.sas.com/rest-apis/mysas), obtain them [here](https://developer.sas.com/rest-apis/mysas/applications) - instructions can be found on the _Authentication_ tab in the same UI [here](https://developer.sas.com/rest-apis/mysas/docs/getting-started/authentication#obtain-client-credentials).
119120
120121
1. Save off the `Client ID` and the `Client Secret` values, which will serve as your API credentials.
121-
> **NOTE:** You only need one of the `Client Secret` values - either will work.
122+
> [!NOTE]
123+
> You only need one of the `Client Secret` values - either will work.
122124
1. Base64 encode each value.
123-
> **NOTE:** When base64 encoding the credentials, take care not to encode
125+
> [!NOTE]
126+
> When base64 encoding the credentials, take care not to encode
124127
> end-of-line characters into the result. <br/> Here is an example of the
125128
> correct way to encode from a Linux command prompt:<br/>
126129
> `echo -n {secret} | base64`<br/><br/> Here is an example of the
@@ -129,10 +132,13 @@ Take the following steps to start using SAS Viya Orders CLI:
129132
1. Add both credentials to your configuration file, or define them as
130133
environment variables:
131134
132-
- Assign the encoded value of `Client ID` to `clientCredentialsId` (if using environment variables, use
133-
`CLIENTCREDENTIALSID`).
134-
- Assign the encoded value of `Client Secret` to `clientCredentialsSecret` (if using environment variables, use
135-
`CLIENTCREDENTIALSSECRET`).
135+
- Assign the encoded value of `Client ID` to `apimClientCredentialsId` (if using environment variables, use
136+
`APIMCLIENTCREDENTIALSID`).
137+
- Assign the encoded value of `Client Secret` to `apimClientCredentialsSecret` (if using environment variables, use
138+
`APIMCLIENTCREDENTIALSSECRET`).
139+
> [!IMPORTANT]
140+
> If you are using keys generated in the [SAS Apigee Developer Portal] (https://api.apiportal.sas.com), the property names to convey those in are `clientCredentialsId` / `CLIENTCREDENTIALSID` and `clientCredentialsSecret` / `CLIENTCREDENTIALSSECRET`. These keys will cease to work on April 20, 2026.
141+
136142
137143
1. Select CLI options. You can then specify them on the command line, pass them
138144
in as environment variables, or include them in a configuration file.
@@ -162,8 +168,8 @@ Take the following steps to start using SAS Viya Orders CLI:
162168
Here is a sample YAML configuration file that contains client credentials:
163169
164170
```
165-
clientCredentialsId: 1a2B3c4D5e6F7h8I9j10K=
166-
clientCredentialsSecret: 4D5e6F7g8H9i==
171+
apimClientCredentialsId: 1a2B3c4D5e6F7h8I9j10K=
172+
apimClientCredentialsSecret: 4D5e6F7g8H9i==
167173
```
168174
169175
### Running
@@ -213,7 +219,7 @@ using SAS Viya Orders CLI:
213219
2020/10/02 19:16:30 Using config file: /sasstuff/.viya4-orders-cli.yaml
214220
OrderNumber: 923456
215221
AssetName: deploymentAssets
216-
AssetReqURL: https://api.sas.com/mysas/orders/923456/cadenceNames/lts/deploymentAssets
222+
AssetReqURL: https://api.apiproxy.sas.com/mysas/orders/923456/cadenceNames/lts/deploymentAssets
217223
AssetLocation: /sasstuff/sasfiles/923456_lts_depassets.tgz
218224
Cadence: Long Term Support 2020.0
219225
CadenceRelease: 20200808.1596943588306
@@ -232,7 +238,7 @@ using SAS Viya Orders CLI:
232238
```text
233239
OrderNumber: 923456
234240
AssetName: license
235-
AssetReqURL: https://api.sas.com/mysas/orders/923456/cadenceNames/lts/cadenceVersions/2020.0/license
241+
AssetReqURL: https://api.apiproxy.sas.com/mysas/orders/923456/cadenceNames/lts/cadenceVersions/2020.0/license
236242
AssetLocation: /auser/vocli/sasfiles/923456_lts_2020.0_license_ren1.jwt
237243
Cadence: Long Term Support 2020.0
238244
CadenceRelease:
@@ -252,7 +258,7 @@ using SAS Viya Orders CLI:
252258
{
253259
"orderNumber": "923457",
254260
"assetName": "certificates",
255-
"assetReqURL": "https://api.sas.com/mysas/orders/923457/certificates",
261+
"assetReqURL": "https://api.apiproxy.sas.com/mysas/orders/923457/certificates",
256262
"assetLocation": "C:\Users\auser\vocli\sasfiles\923457_certs.zip",
257263
"cadence": "",
258264
"cadenceRelease": ""
@@ -270,7 +276,7 @@ using SAS Viya Orders CLI:
270276
```text
271277
OrderNumber: 923457
272278
AssetName: deploymentAssets
273-
AssetReqURL: https://api.sas.com/mysas/orders/923457/cadenceNames/stable/cadenceVersions/2026.01/cadenceReleases/20260127.1769510312235/deploymentAssets
279+
AssetReqURL: https://api.apiproxy.sas.com/mysas/orders/923457/cadenceNames/stable/cadenceVersions/2026.01/cadenceReleases/20260127.1769510312235/deploymentAssets
274280
AssetLocation: /path/to/cwd/SASViyaV4_923457_0_stable_2026.01_20260127.1769510312235_deploymentAssets_1769555752230.tgz
275281
Cadence: Stable 2026.01
276282
CadenceRelease: 20260127.1769510312235

cmd/assethistory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var assetHistoryCmd = &cobra.Command{
2020
Aliases: []string{"ah"},
2121
Args: cobra.RangeArgs(1, 1),
2222
Run: func(cmd *cobra.Command, args []string) {
23-
ar := assetreqs.New(clientID, clientSecret, "assetHistory", args[0], "", "", "", assetFilePath, assetFileName, outFormat, allowUnsuppd)
23+
ar := assetreqs.New(clientCredsType, token, clientID, clientSecret, "assetHistory", args[0], "", "", "", assetFilePath, assetFileName, outFormat, allowUnsuppd)
2424
err := ar.GetAsset()
2525
if err != nil {
2626
log.Fatalln(err)

cmd/certificates.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var certificatesCmd = &cobra.Command{
2020
Args: cobra.ExactArgs(1),
2121
Run: func(cmd *cobra.Command, args []string) {
2222
// Cadence is not a factor in certs, so we hard-code allowUnsuppd to false for the last argument.
23-
ar := assetreqs.New(clientID, clientSecret, "certificates", args[0], "", "", "",
23+
ar := assetreqs.New(clientCredsType, token, clientID, clientSecret, "certificates", args[0], "", "", "",
2424
assetFilePath, assetFileName, outFormat, false)
2525
err := ar.GetAsset()
2626
if err != nil {

cmd/deploymentassets.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var deploymentAssetsCmd = &cobra.Command{
2929
cver = args[2]
3030
crel = args[3]
3131
}
32-
ar := assetreqs.New(clientID, clientSecret, "deploymentAssets", args[0], args[1], cver, crel, assetFilePath, assetFileName, outFormat, allowUnsuppd)
32+
ar := assetreqs.New(clientCredsType, token, clientID, clientSecret, "deploymentAssets", args[0], args[1], cver, crel, assetFilePath, assetFileName, outFormat, allowUnsuppd)
3333
err := ar.GetAsset()
3434
if err != nil {
3535
log.Fatalln(err)

cmd/license.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var licenseCmd = &cobra.Command{
1919
Aliases: []string{"lic"},
2020
Args: cobra.ExactArgs(3),
2121
Run: func(cmd *cobra.Command, args []string) {
22-
ar := assetreqs.New(clientID, clientSecret, "license", args[0], args[1], args[2], "", assetFilePath, assetFileName, outFormat, allowUnsuppd)
22+
ar := assetreqs.New(clientCredsType, token, clientID, clientSecret, "license", args[0], args[1], args[2], "", assetFilePath, assetFileName, outFormat, allowUnsuppd)
2323
err := ar.GetAsset()
2424
if err != nil {
2525
log.Fatalln(err)

cmd/root.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ import (
1212
"unicode"
1313

1414
homedir "github.com/mitchellh/go-homedir"
15+
"github.com/sassoftware/viya4-orders-cli/lib/authn"
1516
"github.com/spf13/cobra"
1617
"github.com/spf13/viper"
1718
)
1819

1920
var (
20-
assetFileName string
21-
assetFilePath string
22-
cfgFile string
23-
outFormat string
24-
clientID string
25-
clientSecret string
26-
allowUnsuppd bool
21+
assetFileName string
22+
assetFilePath string
23+
cfgFile string
24+
outFormat string
25+
clientID string
26+
clientSecret string
27+
clientCredsType string // apigee or apim
28+
token string // only applies to Apigee creds
29+
allowUnsuppd bool
2730
)
2831

2932
// Version is set by the build.
@@ -157,15 +160,45 @@ func usageError(message string) {
157160
}
158161

159162
func setCreds() {
160-
cID, err := base64.StdEncoding.DecodeString(viper.GetString("clientCredentialsId"))
163+
apimCIDProp := "apimClientCredentialsId"
164+
apimCSecProp := "apimClientCredentialsSecret"
165+
// Prefer APIM creds over Apigee.
166+
if viper.IsSet(apimCIDProp) && viper.GetString(apimCIDProp) != "" && viper.IsSet(apimCSecProp) && viper.GetString(apimCSecProp) != "" {
167+
clientCredsType = "apim"
168+
} else {
169+
clientCredsType = "apigee"
170+
}
171+
172+
var cIDProp, cSecProp string
173+
if strings.EqualFold(clientCredsType, "apim") {
174+
cIDProp = apimCIDProp
175+
cSecProp = apimCSecProp
176+
} else {
177+
cIDProp = "clientCredentialsId"
178+
cSecProp = "clientCredentialsSecret"
179+
}
180+
181+
cID, err := base64.StdEncoding.DecodeString(viper.GetString(cIDProp))
161182
if err != nil {
162-
log.Fatalln("ERROR: attempt to decode clientCredentialsId failed: " + err.Error())
183+
log.Fatalln("ERROR: attempt to decode " + cIDProp + " failed: " + err.Error())
163184
}
164-
cSecret, err := base64.StdEncoding.DecodeString(viper.GetString("clientCredentialsSecret"))
185+
cSecret, err := base64.StdEncoding.DecodeString(viper.GetString(cSecProp))
165186
if err != nil {
166-
log.Fatalln("ERROR: attempt to decode clientCredentialsSecret failed: " + err.Error())
187+
log.Fatalln("ERROR: attempt to decode " + cSecProp + " failed: " + err.Error())
167188
}
168189

169190
clientID = strings.TrimRightFunc(string(cID), unicode.IsControl)
170191
clientSecret = strings.TrimRightFunc(string(cSecret), unicode.IsControl)
192+
193+
if clientCredsType == "apigee" {
194+
apigeeAuth()
195+
}
196+
}
197+
198+
func apigeeAuth() {
199+
var err error
200+
token, err = authn.GetBearerToken(clientID, clientSecret)
201+
if err != nil {
202+
log.Fatalln(err.Error())
203+
}
171204
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/mitchellh/go-homedir v1.1.0
77
github.com/spf13/cobra v1.10.2
88
github.com/spf13/viper v1.21.0
9+
golang.org/x/oauth2 v0.36.0
910
)
1011

1112
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
4343
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
4444
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
4545
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
46+
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
47+
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
4648
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
4749
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
4850
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=

lib/assetreqs/assetreqs.go

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,46 @@ import (
2525
const checksumsFile string = "sas-bases/checksums.txt"
2626

2727
const (
28-
viyaOrdersAPIHost string = "https://api.apiproxy.sas.com"
28+
viyaOrdersAPIHost string = "https://api.sas.com"
29+
viyaOrdersAPIAPIMHost string = "https://api.apiproxy.sas.com"
2930
viyaOrdersAPIBasePath string = "/mysas"
3031
viyaOrdersAPIOrdersPath string = "/orders"
3132
)
3233

3334
// AssetReq provides fields that define the parameters of an order asset request.
3435
type AssetReq struct {
35-
clientID string
36-
clientSecret string
37-
aName string
38-
oNum string
39-
cName string
40-
cVer string
41-
cRel string
42-
fPath string
43-
fName string
44-
oFmt string
45-
allowUnsuppd bool
36+
clientCredsType string
37+
token string
38+
clientID string
39+
clientSecret string
40+
aName string
41+
oNum string
42+
cName string
43+
cVer string
44+
cRel string
45+
fPath string
46+
fName string
47+
oFmt string
48+
allowUnsuppd bool
4649
}
4750

4851
// New initializes an AssetReq struct.
49-
func New(cID, cSec, assetName, orderNum, cadenceName, cadenceVer, cadenceRel, filePath,
52+
func New(credsType, token, cID, cSec, assetName, orderNum, cadenceName, cadenceVer, cadenceRel, filePath,
5053
fileName, outputFormat string, allowUnsuppd bool) (ar AssetReq) {
5154
return AssetReq{
52-
clientID: cID,
53-
clientSecret: cSec,
54-
aName: assetName,
55-
oNum: orderNum,
56-
cName: cadenceName,
57-
cVer: cadenceVer,
58-
cRel: cadenceRel,
59-
fPath: filePath,
60-
fName: fileName,
61-
oFmt: outputFormat,
62-
allowUnsuppd: allowUnsuppd,
55+
clientCredsType: credsType,
56+
token: token,
57+
clientID: cID,
58+
clientSecret: cSec,
59+
aName: assetName,
60+
oNum: orderNum,
61+
cName: cadenceName,
62+
cVer: cadenceVer,
63+
cRel: cadenceRel,
64+
fPath: filePath,
65+
fName: fileName,
66+
oFmt: outputFormat,
67+
allowUnsuppd: allowUnsuppd,
6368
}
6469
}
6570

@@ -177,9 +182,15 @@ func (ar AssetReq) buildReq() (req *http.Request, err error) {
177182
return req, errors.New("ERROR: setup of asset request failed: " + err.Error())
178183
}
179184

180-
// Use direct assignment to preserve exact header casing (bypasses canonicalization)
181-
req.Header["ClientId"] = []string{ar.clientID}
182-
req.Header["ClientSecret"] = []string{ar.clientSecret}
185+
// Set the appropriate authentication headers depending on the type of client credentials being used.
186+
if ar.clientCredsType == "apim" {
187+
// Use direct assignment to preserve exact header casing (bypasses canonicalization)
188+
req.Header["ClientId"] = []string{ar.clientID}
189+
req.Header["ClientSecret"] = []string{ar.clientSecret}
190+
} else {
191+
bearer := "Bearer " + ar.token
192+
req.Header.Set("Authorization", bearer)
193+
}
183194

184195
// If the allowUnsupported option was used, pass along allowUnsupported=true as a query param on the API call.
185196
if ar.allowUnsuppd {
@@ -194,7 +205,13 @@ func (ar AssetReq) buildReq() (req *http.Request, err error) {
194205

195206
// buildURL builds the request URL.
196207
func (ar AssetReq) buildURL() (urlStr string, err error) {
197-
u, err := url.ParseRequestURI(viyaOrdersAPIHost)
208+
var host string
209+
if ar.clientCredsType == "apim" {
210+
host = viyaOrdersAPIAPIMHost
211+
} else {
212+
host = viyaOrdersAPIHost
213+
}
214+
u, err := url.ParseRequestURI(host)
198215
if err != nil {
199216
return urlStr, errors.New("ERROR: attempt to parse asset request URI failed: " + err.Error())
200217
}

lib/authn/authn.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright © 2020-2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package authn provides a func that will exchange OAuth client credentials for a Bearer token that will expire after
5+
// 30 minutes.
6+
package authn
7+
8+
import (
9+
"context"
10+
"errors"
11+
"fmt"
12+
"net/url"
13+
"strings"
14+
15+
"golang.org/x/oauth2"
16+
"golang.org/x/oauth2/clientcredentials"
17+
)
18+
19+
const (
20+
viyaOrdersAPIHost string = "https://api.sas.com"
21+
viyaOrdersAPIBasePath string = "/mysas"
22+
viyaOrdersAPITokenPath string = "/token"
23+
)
24+
25+
// GetBearerToken calls the /token SAS Viya Orders API endpoint to exchange client credentials for a Bearer token.
26+
// The client credentials are obtained from the SAS API Portal (https://apiportal.sas.com), and should be defined in
27+
// Viper (https://github.com/spf13/viper) as clientCredentialsId (key) and clientCredentialsSecret (secret).
28+
func GetBearerToken(cID, cSec string) (token string, err error) {
29+
// id, err := base64.StdEncoding.DecodeString(viper.GetString("clientCredentialsId"))
30+
// if err != nil {
31+
// return token, errors.New("ERROR: attempt to decode clientCredentialsId failed: " + err.Error())
32+
// }
33+
// sec, err := base64.StdEncoding.DecodeString(viper.GetString("clientCredentialsSecret"))
34+
// if err != nil {
35+
// return token, errors.New("ERROR: attempt to decode clientCredentialsSecret failed: " + err.Error())
36+
// }
37+
38+
// Build the request URL.
39+
u, err := url.ParseRequestURI(viyaOrdersAPIHost)
40+
if err != nil {
41+
return token, errors.New("ERROR: attempt to parse Bearer token request URI failed: " + err.Error())
42+
}
43+
44+
var b strings.Builder
45+
fmt.Fprintf(&b, "%s", viyaOrdersAPIBasePath)
46+
fmt.Fprintf(&b, "%s", viyaOrdersAPITokenPath)
47+
u.Path = b.String()
48+
urlStr := u.String()
49+
50+
oauthCfg := &clientcredentials.Config{
51+
ClientID: cID,
52+
ClientSecret: cSec,
53+
TokenURL: urlStr,
54+
AuthStyle: oauth2.AuthStyleAutoDetect,
55+
}
56+
57+
oaToken, err := oauthCfg.Token(context.Background())
58+
if err != nil {
59+
return token, errors.New("ERROR: Bearer token request failed: " + err.Error())
60+
}
61+
token = oaToken.AccessToken
62+
63+
return token, nil
64+
}

0 commit comments

Comments
 (0)