mirror of
https://github.com/clearlinux/rkt.git
synced 2026-08-19 05:27:23 +00:00
functional tests: Import test-aci-auth-server
It is now known as test-auth-server and it contains an aci subpackage. Later we can add a docker package to test docker auth.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
Before running `test-aci-auth-server` make sure that `go` and `actool` are in your
|
||||
`$PATH`.
|
||||
|
||||
```
|
||||
$ ./test-aci-auth-server start basic
|
||||
|
||||
{
|
||||
"rktKind": "auth",
|
||||
"rktVersion": "v1",
|
||||
"domains": ["127.0.0.1:48608"],
|
||||
"type": "basic",
|
||||
"credentials":
|
||||
{
|
||||
"user": "bar",
|
||||
"password": "baz"
|
||||
}
|
||||
}
|
||||
|
||||
Ready, waiting for connections at https://127.0.0.1:48608
|
||||
```
|
||||
|
||||
(You can run `test-aci-auth-server start` with either `none`, `basic` or `oauth` parameter)
|
||||
|
||||
Copy the snippet to `/etc/rkt/auth.d/test.json` and run `rkt
|
||||
--insecure-skip-verify run
|
||||
https://127.0.0.1:48608/<WHATEVER>/prog.aci`. The `rkt` output ought
|
||||
to be something like:
|
||||
```
|
||||
# rkt --insecure-skip-verify run https://127.0.0.1:48608/basic1/prog.aci
|
||||
rkt: fetching image from https://127.0.0.1:48608/basic1/prog.aci
|
||||
|
||||
|
||||
|
||||
Authentication succeeded.
|
||||
Sending SIGTERM to remaining processes...
|
||||
Sending SIGKILL to remaining processes...
|
||||
Unmounting file systems.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/pts.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/shm.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/sys.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/proc.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/console.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/tty.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/urandom.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/random.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/full.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/zero.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs/dev/null.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs.
|
||||
Unmounting /proc/sys/kernel/random/boot_id.
|
||||
Unmounting /opt/stage2/sha512-82d0d76f85d04a73e17a377c304ffbd8/rootfs.
|
||||
All filesystems unmounted.
|
||||
Halting system.
|
||||
```
|
||||
|
||||
While the additional output from `test-aci-auth-server`:
|
||||
```
|
||||
Trying to serve "/basic10/prog.aci"
|
||||
serving
|
||||
done.
|
||||
```
|
||||
|
||||
The `test-aci-auth-server start` with `oauth` will print something like this:
|
||||
```
|
||||
$ ./test-aci-auth-server start oauth
|
||||
|
||||
{
|
||||
"rktKind": "auth",
|
||||
"rktVersion": "v1",
|
||||
"domains": ["127.0.0.1:48805"],
|
||||
"type": "oauth",
|
||||
"credentials":
|
||||
{
|
||||
"token": "sometoken"
|
||||
}
|
||||
}
|
||||
|
||||
Ready, waiting for connections at https://127.0.0.1:48805
|
||||
```
|
||||
|
||||
To stop the `test-aci-auth-server` run stop command:
|
||||
```
|
||||
$ ./test-aci-auth-server stop https://127.0.0.1:60268
|
||||
Response status: 200 OK
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package aci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type aciToolkit struct {
|
||||
acTool string
|
||||
goTool string
|
||||
}
|
||||
|
||||
func (t *aciToolkit) prepareACI() ([]byte, error) {
|
||||
dir, err := t.createTree()
|
||||
if dir != "" {
|
||||
defer os.RemoveAll(dir)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build ACI tree: %v", err)
|
||||
}
|
||||
if err := t.buildProg(dir); err != nil {
|
||||
return nil, fmt.Errorf("failed to build test program: %v", err)
|
||||
}
|
||||
fn, err := t.buildACI(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build ACI: %v", err)
|
||||
}
|
||||
defer os.Remove(fn)
|
||||
contents, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read ACI to memory: %v", err)
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
const (
|
||||
manifestStr = `{"acKind":"ImageManifest","acVersion":"0.5.1+git","name":"testprog","app":{"exec":["/prog"],"user":"0","group":"0"}}`
|
||||
testProgSrcStr = `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Authentication succeeded.")
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func (t *aciToolkit) createTree() (string, error) {
|
||||
aciDir := "ACI"
|
||||
rootDir := filepath.Join(aciDir, "rootfs")
|
||||
manifestFile := filepath.Join(aciDir, "manifest")
|
||||
srcFile := filepath.Join(rootDir, "prog.go")
|
||||
if err := os.Mkdir(aciDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create ACI directory: %v", err)
|
||||
}
|
||||
if err := os.Mkdir(rootDir, 0755); err != nil {
|
||||
return aciDir, fmt.Errorf("failed to create rootfs directory: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(manifestFile, []byte(manifestStr), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write manifest: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(srcFile, []byte(testProgSrcStr), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write go source: %v", err)
|
||||
}
|
||||
return aciDir, nil
|
||||
}
|
||||
|
||||
func (t *aciToolkit) buildProg(aciDir string) error {
|
||||
args := []string{
|
||||
"go",
|
||||
"build",
|
||||
"-o",
|
||||
"prog",
|
||||
"./prog.go",
|
||||
}
|
||||
dir := filepath.Join(aciDir, "rootfs")
|
||||
return runTool(t.goTool, args, dir)
|
||||
}
|
||||
|
||||
func (t *aciToolkit) buildACI(aciDir string) (string, error) {
|
||||
timedata, err := time.Now().MarshalBinary()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize current date to bytes: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(aciDir, "rootfs", "stamp"), timedata, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write a stamp: %v", err)
|
||||
}
|
||||
fn := "prog-build.aci"
|
||||
args := []string{
|
||||
"actool",
|
||||
"build",
|
||||
aciDir,
|
||||
fn,
|
||||
}
|
||||
if err := runTool(t.acTool, args, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func runTool(tool string, args []string, dir string) error {
|
||||
outBuf := new(bytes.Buffer)
|
||||
errBuf := new(bytes.Buffer)
|
||||
cmd := exec.Cmd{
|
||||
Path: tool,
|
||||
Args: args,
|
||||
Dir: dir,
|
||||
Stdout: outBuf,
|
||||
Stderr: errBuf,
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to execute `%s %s`: %v\nstdout:\n%v\n\nstderr:\n%v)", args[0], args[1], err, outBuf.String(), errBuf.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package aci
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func StartServer(auth Type) (*Server, error) {
|
||||
return NewServer(auth, 10)
|
||||
}
|
||||
|
||||
func StopServer(host string) (*http.Response, error) {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
res, err := client.Post(host, "whatever", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send post to %q: %v", host, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package aci
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Type int
|
||||
|
||||
const (
|
||||
None Type = iota
|
||||
Basic
|
||||
Oauth
|
||||
)
|
||||
|
||||
type httpError struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *httpError) Error() string {
|
||||
return fmt.Sprintf("%d: %s", e.code, e.message)
|
||||
}
|
||||
|
||||
type serverHandler struct {
|
||||
auth Type
|
||||
stop chan<- struct{}
|
||||
msg chan<- string
|
||||
tools *aciToolkit
|
||||
}
|
||||
|
||||
func (h *serverHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
h.stop <- struct{}{}
|
||||
return
|
||||
case "GET":
|
||||
// handled later
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
switch h.auth {
|
||||
case None:
|
||||
// no auth to do.
|
||||
case Basic:
|
||||
payload, httpErr := getAuthPayload(r, "Basic")
|
||||
if httpErr != nil {
|
||||
w.WriteHeader(httpErr.code)
|
||||
h.sendMsg(fmt.Sprintf(`No "Authorization" header: %v`, httpErr.message))
|
||||
return
|
||||
}
|
||||
creds, err := base64.StdEncoding.DecodeString(string(payload))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
h.sendMsg(fmt.Sprintf(`Badly formed "Authorization" header`))
|
||||
return
|
||||
}
|
||||
parts := strings.Split(string(creds), ":")
|
||||
if len(parts) != 2 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
h.sendMsg(fmt.Sprintf(`Badly formed "Authorization" header (2)`))
|
||||
return
|
||||
}
|
||||
user := parts[0]
|
||||
password := parts[1]
|
||||
if user != "bar" || password != "baz" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
h.sendMsg(fmt.Sprintf("Bad credentials: %q", string(creds)))
|
||||
return
|
||||
}
|
||||
case Oauth:
|
||||
payload, httpErr := getAuthPayload(r, "Bearer")
|
||||
if httpErr != nil {
|
||||
w.WriteHeader(httpErr.code)
|
||||
h.sendMsg(fmt.Sprintf(`No "Authorization" header: %v`, httpErr.message))
|
||||
return
|
||||
}
|
||||
if payload != "sometoken" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
h.sendMsg(fmt.Sprintf(`Bad token: %q`, payload))
|
||||
return
|
||||
}
|
||||
default:
|
||||
panic("Woe is me!")
|
||||
}
|
||||
h.sendMsg(fmt.Sprintf("Trying to serve %q", r.URL.String()))
|
||||
switch filepath.Base(r.URL.Path) {
|
||||
case "prog.aci":
|
||||
h.sendMsg(fmt.Sprintf(" serving"))
|
||||
if data, err := h.tools.prepareACI(); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.sendMsg(fmt.Sprintf(" failed (%v)", err))
|
||||
} else {
|
||||
w.Write(data)
|
||||
h.sendMsg(fmt.Sprintf(" done."))
|
||||
}
|
||||
default:
|
||||
h.sendMsg(fmt.Sprintf(" not found."))
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *serverHandler) sendMsg(msg string) {
|
||||
select {
|
||||
case h.msg <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthPayload(r *http.Request, authType string) (string, *httpError) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
err := &httpError{
|
||||
code: http.StatusUnauthorized,
|
||||
message: "No auth",
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
parts := strings.Split(auth, " ")
|
||||
if len(parts) != 2 {
|
||||
err := &httpError{
|
||||
code: http.StatusBadRequest,
|
||||
message: "Malformed auth",
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if parts[0] != authType {
|
||||
err := &httpError{
|
||||
code: http.StatusUnauthorized,
|
||||
message: "Wrong auth",
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Stop <-chan struct{}
|
||||
Msg <-chan string
|
||||
Conf string
|
||||
URL string
|
||||
handler *serverHandler
|
||||
http *httptest.Server
|
||||
}
|
||||
|
||||
func (s *Server) Close() {
|
||||
s.http.Close()
|
||||
close(s.handler.msg)
|
||||
close(s.handler.stop)
|
||||
}
|
||||
|
||||
func NewServer(auth Type, msgCapacity int) (*Server, error) {
|
||||
return NewServerWithPaths(auth, msgCapacity, "actool", "go")
|
||||
}
|
||||
|
||||
func NewServerWithPaths(auth Type, msgCapacity int, acTool, goTool string) (*Server, error) {
|
||||
if !filepath.IsAbs(acTool) {
|
||||
absAcTool, err := getTool(acTool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acTool = absAcTool
|
||||
}
|
||||
if !filepath.IsAbs(goTool) {
|
||||
absGoTool, err := getTool(goTool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
goTool = absGoTool
|
||||
}
|
||||
stop := make(chan struct{})
|
||||
msg := make(chan string, msgCapacity)
|
||||
server := &Server{
|
||||
Stop: stop,
|
||||
Msg: msg,
|
||||
handler: &serverHandler{
|
||||
auth: auth,
|
||||
stop: stop,
|
||||
msg: msg,
|
||||
tools: &aciToolkit{
|
||||
acTool: acTool,
|
||||
goTool: goTool,
|
||||
},
|
||||
},
|
||||
}
|
||||
server.http = httptest.NewUnstartedServer(server.handler)
|
||||
server.http.TLS = &tls.Config{InsecureSkipVerify: true}
|
||||
server.http.StartTLS()
|
||||
server.URL = server.http.URL
|
||||
host := server.http.Listener.Addr().String()
|
||||
switch auth {
|
||||
case None:
|
||||
// nothing to do
|
||||
case Basic:
|
||||
creds := `"user": "bar",
|
||||
"password": "baz"`
|
||||
server.Conf = sprintCreds(host, "basic", creds)
|
||||
case Oauth:
|
||||
creds := `"token": "sometoken"`
|
||||
server.Conf = sprintCreds(host, "oauth", creds)
|
||||
default:
|
||||
panic("Woe is me!")
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func getTool(tool string) (string, error) {
|
||||
toolPath, err := exec.LookPath(tool)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find %s in $PATH: $v", tool, err)
|
||||
}
|
||||
absToolPath, err := filepath.Abs(toolPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path of %s: %v", tool, err)
|
||||
}
|
||||
return absToolPath, nil
|
||||
}
|
||||
|
||||
func sprintCreds(host, auth, creds string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"rktKind": "auth",
|
||||
"rktVersion": "v1",
|
||||
"domains": ["%s"],
|
||||
"type": "%s",
|
||||
"credentials":
|
||||
{
|
||||
%s
|
||||
}
|
||||
}
|
||||
|
||||
`, host, auth, creds)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
taas "github.com/coreos/rkt/tests/test-auth-server/aci"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmdsStr := "start, stop"
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Printf("Error: expected a command - %s\n", cmdsStr)
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "start":
|
||||
err = start(os.Args[2:])
|
||||
case "stop":
|
||||
err = stop(os.Args[2:])
|
||||
default:
|
||||
err = fmt.Errorf("wrong command %q, should be %s", os.Args[1], cmdsStr)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func start(args []string) error {
|
||||
typesStr := "none, basic, oauth"
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("expected a type - %s", typesStr)
|
||||
}
|
||||
types := map[string]taas.Type{
|
||||
"none": taas.None,
|
||||
"basic": taas.Basic,
|
||||
"oauth": taas.Oauth,
|
||||
}
|
||||
auth, ok := types[args[0]]
|
||||
if !ok {
|
||||
return fmt.Errorf("wrong type %q, should, be %s", args[0], typesStr)
|
||||
}
|
||||
server, err := taas.StartServer(auth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
if server.Conf != "" {
|
||||
fmt.Printf(server.Conf)
|
||||
}
|
||||
fmt.Printf("Ready, waiting for connections at %s\n", server.URL)
|
||||
loop(server)
|
||||
fmt.Println("Byebye")
|
||||
return nil
|
||||
}
|
||||
|
||||
func loop(server *taas.Server) {
|
||||
for {
|
||||
select {
|
||||
case <-server.Stop:
|
||||
server.Close()
|
||||
return
|
||||
case msg, ok := <-server.Msg:
|
||||
if ok {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("expected a host")
|
||||
}
|
||||
host := args[0]
|
||||
res, err := taas.StopServer(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop server: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
fmt.Printf("Response status: %s\n", res.Status)
|
||||
if res.StatusCode/100 != 2 {
|
||||
return fmt.Errorf("got a nonsuccess status")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user