diff --git a/.gitignore b/.gitignore
index 56be033..ff0306a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
# customize
.cookie.yaml
-harbor-go-client
+harborctl*
*.tar.gz
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8d6de5..04fabdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+
+# [1.0.0](https://github.com/moooofly/harbor-go-client/compare/v0.9.6...v1.0.0) (2018-10-24)
+
+
+### Features
+
+* add replications trigger API ([0753d6c](https://github.com/moooofly/harbor-go-client/commit/0753d6c))
+* add syncregistry and email_ping APIs ([2b18258](https://github.com/moooofly/harbor-go-client/commit/2b18258))
+* add usergroups APIs ([47ef550](https://github.com/moooofly/harbor-go-client/commit/47ef550))
+* support both linux and darwin platform compilation ([a665e0f](https://github.com/moooofly/harbor-go-client/commit/a665e0f))
+* support darwin platform, fix [#1](https://github.com/moooofly/harbor-go-client/issues/1) ([c671c20](https://github.com/moooofly/harbor-go-client/commit/c671c20))
+
+
+
# [0.9.6](https://github.com/moooofly/harbor-go-client/compare/v0.9.5...v0.9.6) (2018-10-17)
diff --git a/Makefile b/Makefile
index 00374c1..b60ea46 100644
--- a/Makefile
+++ b/Makefile
@@ -5,11 +5,14 @@ ifeq "$(GOPATH)" ""
$(error Please set the environment variable GOPATH before running `make`)
endif
-GO := go
-PKGS := $(shell $(GO) list ./... | grep -v vendor)
+GOOS := $(shell go env GOOS)
+GOARCH := $(shell go env GOARCH)
+PKGS := $(shell go list ./... | grep -v vendor)
+
+
# NOTE: '-race' requires cgo; enable cgo by setting CGO_ENABLED=1
#BUILD_FLAG := -race
-GOBUILD := CGO_ENABLED=0 $(GO) build $(BUILD_FLAG)
+GOBUILD := CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build $(BUILD_FLAG)
LDFLAGS += -X "github.com/moooofly/harbor-go-client/utils.ClientVersion=$(shell cat VERSION)"
LDFLAGS += -X "github.com/moooofly/harbor-go-client/utils.GoVersion=$(shell go version)"
@@ -24,12 +27,12 @@ all: lint build test
build:
@echo "==> Building ..."
- $(GOBUILD) -ldflags '$(LDFLAGS)' ./
+ $(GOBUILD) -o harborctl_${GOOS}_${GOARCH} -ldflags '$(LDFLAGS)' ./
@echo ""
install:
@echo "==> Installing ..."
- $(GO) install -x ${SRC}
+ go install -x ${SRC}
@echo ""
lint:
@@ -43,19 +46,27 @@ lint:
test:
@echo "==> Testing ..."
- $(GO) test -short -race $(PKGS)
+ go test -short -race $(PKGS)
@echo ""
-pack: build
+build_linux:
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILD_FLAG) -o harborctl_linux_amd64 -ldflags '$(LDFLAGS)' ./
+
+build_darwin:
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(BUILD_FLAG) -o harborctl_darwin_amd64 -ldflags '$(LDFLAGS)' ./
+
+pack: build_linux build_darwin
@echo "==> Packing ..."
- @tar czvf $(shell cat VERSION)-bin.tar.gz harbor-go-client conf/*.yaml
+ @tar czvf harborctl-$(shell cat VERSION).linux-amd64.tar.gz harborctl_linux_amd64 conf/*.yaml
@echo ""
- @rm harbor-go-client
+ @tar czvf harborctl-$(shell cat VERSION).darwin-amd64.tar.gz harborctl_darwin_amd64 conf/*.yaml
@echo ""
+ @rm harborctl_linux_amd64
+ @rm harborctl_darwin_amd64
docker:
@echo "==> Creating docker image ..."
- docker build -t harbor-go-client:$(shell git rev-parse --short HEAD) .
+ docker build -t harborctl:$(shell git rev-parse --short HEAD) .
@echo ""
misspell:
@@ -73,7 +84,8 @@ shellcheck:
clean:
@echo "==> Cleaning ..."
- $(GO) clean -x -i ${SRC}
+ go clean -x -i ${SRC}
+ rm -f harborctl_*
rm -rf *.out
rm -rf *.tar.gz
@echo ""
diff --git a/README.md b/README.md
index 9a5d2d1..85dbeb5 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ Current Harbor API support status:
- [x] GET /api/labels/{id}
- [x] PUT /api/labels/{id}
- replications
- - [ ] POST /api/replications
+ - [x] POST /api/replications
- targets
- [x] GET /api/targets
- [x] POST /api/targets
@@ -104,7 +104,7 @@ Current Harbor API support status:
- [x] PUT /api/targets/{id}
- [x] GET /api/targets/{id}/policies/
- internal
- - [ ] POST /api/internal/syncregistry
+ - [x] POST /api/internal/syncregistry
- systeminfo
- [x] GET /api/systeminfo
- [x] GET /api/systeminfo/volumes
@@ -115,17 +115,17 @@ Current Harbor API support status:
- [ ] GET /api/ldap/users/search
- [ ] POST /api/ldap/users/import
- usergroups
- - [ ] GET /api/usergroups
- - [ ] POST /api/usergroups
- - [ ] DELETE /api/usergroups/{group_id}
- - [ ] GET /api/usergroups/{group_id}
- - [ ] PUT /api/usergroups/{group_id}
+ - [x] GET /api/usergroups
+ - [x] POST /api/usergroups
+ - [x] DELETE /api/usergroups/{group_id}
+ - [x] GET /api/usergroups/{group_id}
+ - [x] PUT /api/usergroups/{group_id}
- configurations
- [x] GET /api/configurations
- [x] PUT /api/configurations
- [x] POST /api/configurations/reset
- email
- - [ ] POST /api/email/ping
+ - [x] POST /api/email/ping
Additional features supported:
diff --git a/VERSION b/VERSION
index c7ceed8..3eefcb9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v0.9.6
+1.0.0
diff --git a/api/others.go b/api/others.go
index 3cb1c6c..b6cab43 100644
--- a/api/others.go
+++ b/api/others.go
@@ -1,4 +1,118 @@
package api
-// POST /email/ping
-// POST /internal/syncregistry
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/moooofly/harbor-go-client/utils"
+)
+
+func init() {
+ utils.Parser.AddCommand("syncregistry",
+ "Sync repositories from registry to DB.",
+ "This endpoint is for syncing all repositories of registry with database.",
+ &syncregistry)
+ utils.Parser.AddCommand("email_ping",
+ "Test connection and authentication with email server.",
+ "Test connection and authentication with email server.",
+ &emailping)
+}
+
+type syncRegistry struct {
+}
+
+var syncregistry syncRegistry
+
+func (x *syncRegistry) Execute(args []string) error {
+ PostSyncRegistry(utils.URLGen("/api/internal/syncregistry"))
+ return nil
+}
+
+// PostSyncRegistry is for syncing all repositories of registry with database.
+//
+// params:
+//
+// format:
+// POST /internal/syncregistry
+//
+// e.g. curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' 'https://localhost/api/internal/syncregistry'
+func PostSyncRegistry(baseURL string) {
+ targetURL := baseURL
+ fmt.Println("==> POST", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ utils.Request.Post(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ End(utils.PrintStatus)
+}
+
+type emailPing struct {
+ EmailHost string `short:"h" long:"email_host" description:"The host of email server." default:"smtp.mydomain.com" json:"email_host"`
+ EmailPort int `short:"t" long:"email_port" description:"The port of email server." default:"25" json:"email_port"`
+ EmailUsername string `short:"u" long:"email_username" description:"The username of email server." default:"sample_admin@mydomain.com" json:"email_username"`
+ EmailPassword string `short:"p" long:"email_password" description:"The password of email server." default:"" json:"email_password"`
+ EmailSsl bool `short:"s" long:"email_ssl" description:"Use ssl/tls or not." json:"email_ssl"`
+ EmailIdentity string `short:"i" long:"email_identity" description:"The identity of email server." default:"" json:"email_identity"`
+}
+
+var emailping emailPing
+
+func (x *emailPing) Execute(args []string) error {
+ PostEmailPing(utils.URLGen("/api/email/ping"))
+ return nil
+}
+
+// PostEmailPing tests connection and authentication with email server.
+//
+// params:
+// email_host - The host of email server.
+// email_port - The port of email server.
+// email_username - The username of email server.
+// email_password - The password of email server.
+// email_ssl - Use ssl/tls or not.
+// email_identity - The dentity of email server.
+//
+// format:
+// POST /email/ping
+//
+// e.g.
+/*
+curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' -d '{ \
+ "email_host": "string", \
+ "email_port": 0, \
+ "email_username": "string", \
+ "email_password": "string", \
+ "email_ssl": true, \
+ "email_identity": "string" \
+ }' 'https://localhost/api/email/ping'
+)*/
+func PostEmailPing(baseURL string) {
+ targetURL := baseURL
+ fmt.Println("==> POST", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ t, err := json.Marshal(&emailping)
+ if err != nil {
+ fmt.Println("error:", err)
+ return
+ }
+
+ fmt.Println("==> email ping:", string(t))
+
+ utils.Request.Post(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ Send(string(t)).
+ End(utils.PrintStatus)
+}
diff --git a/api/replications.go b/api/replications.go
new file mode 100644
index 0000000..bc366ff
--- /dev/null
+++ b/api/replications.go
@@ -0,0 +1,63 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/moooofly/harbor-go-client/utils"
+)
+
+func init() {
+ utils.Parser.AddCommand("replication_trigger_by_id",
+ "Trigger the replication according to the specified policy.",
+ "This endpoint is used to trigger a replication.",
+ &replTriByID)
+}
+
+type replicationTriByID struct {
+ PolicyID int `short:"i" long:"policy_id" description:"(REQUIRED) The ID of replication policy" required:"yes" json:"policy_id"`
+}
+
+var replTriByID replicationTriByID
+
+func (x *replicationTriByID) Execute(args []string) error {
+ PostReplTriByID(utils.URLGen("/api/replications"))
+ return nil
+}
+
+// PostReplTriByID is used to trigger a replication.
+//
+// params:
+// policy_id - (REQUIRED) The ID of replication policy
+//
+// format:
+// POST /replications
+//
+// e.g.
+/*
+ curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' -d '{ \
+ "policy_id": 1 \
+ }' 'https://localhost/api/replications'
+*/
+func PostReplTriByID(baseURL string) {
+ targetURL := baseURL
+ fmt.Println("==> POST", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ t, err := json.Marshal(&replTriByID)
+ if err != nil {
+ fmt.Println("error:", err)
+ return
+ }
+
+ utils.Request.Post(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ Send(string(t)).
+ End(utils.PrintStatus)
+}
diff --git a/api/usergroups.go b/api/usergroups.go
new file mode 100644
index 0000000..76129f0
--- /dev/null
+++ b/api/usergroups.go
@@ -0,0 +1,241 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+
+ "github.com/moooofly/harbor-go-client/utils"
+)
+
+func init() {
+ utils.Parser.AddCommand("usergroups_list",
+ "Get all user groups information",
+ "Get all user groups information",
+ &ugList)
+ utils.Parser.AddCommand("usergroup_create",
+ "Create user group",
+ "Create user group information",
+ &ugCreate)
+ utils.Parser.AddCommand("usergroup_del",
+ "Delete user group",
+ "Delete user group",
+ &ugDel)
+ utils.Parser.AddCommand("usergroup_get",
+ "Get user group information",
+ "Get user group information",
+ &ugGet)
+ utils.Parser.AddCommand("usergroup_update",
+ "Update group information",
+ "Update group information",
+ &ugUpdate)
+}
+
+type usergroupsList struct {
+}
+
+var ugList usergroupsList
+
+func (x *usergroupsList) Execute(args []string) error {
+ GetUsergroupsList(utils.URLGen("/api/usergroups"))
+ return nil
+}
+
+// GetUsergroupsList get all user groups information
+//
+// params:
+//
+// e.g. curl -X GET --header 'Accept: application/json' 'https://localhost/api/usergroups'
+func GetUsergroupsList(baseURL string) {
+ targetURL := baseURL
+ fmt.Println("==> GET", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ utils.Request.Get(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ End(utils.PrintStatus)
+}
+
+type usergroupCreate struct {
+ ID int `short:"i" long:"id" description:"The ID of the user group" default:"0" json:"id"`
+ GroupName string `short:"n" long:"group_name" description:"The name of the user group" default:"tmp-group" json:"group_name"`
+ GroupType int `short:"t" long:"group_type" description:"The group type, 1 for LDAP group." default:"1" json:"group_type"`
+ LDAPGroupDN string `short:"l" long:"ldap_group_dn" description:"The DN of the LDAP group if group type is 1 (LDAP group)." default:"" json:"ldap_group_dn"`
+}
+
+var ugCreate usergroupCreate
+
+func (x *usergroupCreate) Execute(args []string) error {
+ PostUsergroupCreate(utils.URLGen("/api/usergroups"))
+ return nil
+}
+
+// PostUsergroupCreate create user group information
+//
+// params:
+// id - The ID of the user group
+// group_name - The name of the user group
+// group_type - The group type, 1 for LDAP group
+// ldap_group_dn - The DN of the LDAP group if group type is 1 (LDAP group)
+//
+// e.g.
+/*
+curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' -d '{ \
+ "id": 100, \
+ "group_name": "tmp-group", \
+ "group_type": 1, \
+ "ldap_group_dn": "" \
+ }' 'https://localhost/api/usergroups'
+*/
+func PostUsergroupCreate(baseURL string) {
+ targetURL := baseURL
+ fmt.Println("==> POST", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ t, err := json.Marshal(&ugCreate)
+ if err != nil {
+ fmt.Println("error:", err)
+ return
+ }
+
+ fmt.Println("===> usergroup create:", string(t))
+
+ utils.Request.Post(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ Send(string(t)).
+ End(utils.PrintStatus)
+}
+
+type usergroupDel struct {
+ ID int `short:"i" long:"id" description:"(REQUIRED) The ID of the user group" required:"yes"`
+}
+
+var ugDel usergroupDel
+
+func (x *usergroupDel) Execute(args []string) error {
+ DeleteUsergroup(utils.URLGen("/api/usergroups"))
+ return nil
+}
+
+// DeleteUsergroup delete user group
+//
+// params:
+// id - (REQUIRED) The ID of the user group
+//
+// e.g. curl -X DELETE --header 'Accept: text/plain' 'https://localhost/api/usergroups/1'
+func DeleteUsergroup(baseURL string) {
+ targetURL := baseURL + "/" + strconv.Itoa(ugDel.ID)
+ fmt.Println("==> DELETE", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ utils.Request.Delete(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ End(utils.PrintStatus)
+}
+
+type usergroupGet struct {
+ ID int `short:"i" long:"id" description:"(REQUIRED) The ID of the user group" required:"yes"`
+}
+
+var ugGet usergroupGet
+
+func (x *usergroupGet) Execute(args []string) error {
+ GetUsergroup(utils.URLGen("/api/usergroups"))
+ return nil
+}
+
+// GetUsergroup get user group information
+//
+// params:
+// id - (REQUIRED) The ID of the user group
+//
+// e.g. curl -X GET --header 'Accept: text/plain' 'https://localhost/api/usergroups/1'
+func GetUsergroup(baseURL string) {
+ targetURL := baseURL + "/" + strconv.Itoa(ugGet.ID)
+ fmt.Println("==> GET", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ utils.Request.Get(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ End(utils.PrintStatus)
+}
+
+type usergroupUpdate struct {
+ ID int `short:"i" long:"id" description:"The ID of the user group" default:"0" json:"id"`
+ GroupName string `short:"n" long:"group_name" description:"The name of the user group" default:"tmp-group" json:"group_name"`
+ GroupType int `short:"t" long:"group_type" description:"The group type, 1 for LDAP group." default:"1" json:"group_type"`
+ LDAPGroupDN string `short:"l" long:"ldap_group_dn" description:"The DN of the LDAP group if group type is 1 (LDAP group)." default:"" json:"ldap_group_dn"`
+}
+
+var ugUpdate usergroupUpdate
+
+func (x *usergroupUpdate) Execute(args []string) error {
+ PutUsergroup(utils.URLGen("/api/usergroups"))
+ return nil
+}
+
+// PutUsergroup update user group information
+//
+// params:
+// id - The ID of the user group
+// group_name - The name of the user group
+// group_type - The group type, 1 for LDAP group
+// ldap_group_dn - The DN of the LDAP group if group type is 1 (LDAP group)
+//
+// e.g.
+/*
+curl -X PUT --header 'Content-Type: application/json' --header 'Accept: text/plain' -d '{ \
+ "id": 1, \
+ "group_name": "tmp-group", \
+ "group_type": 1, \
+ "ldap_group_dn": "" \
+ }' 'https://localhost/api/usergroups/1'
+*/
+func PutUsergroup(baseURL string) {
+ targetURL := baseURL + "/" + strconv.Itoa(ugUpdate.ID)
+ fmt.Println("==> PUT", targetURL)
+
+ // Read beegosessionID from .cookie.yaml
+ c, err := utils.CookieLoad()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ t, err := json.Marshal(&ugUpdate)
+ if err != nil {
+ fmt.Println("error:", err)
+ return
+ }
+
+ fmt.Println("===> usergroup update:", string(t))
+
+ utils.Request.Put(targetURL).
+ Set("Cookie", "harbor-lang=zh-cn; beegosessionID="+c.BeegosessionID).
+ Send(string(t)).
+ End(utils.PrintStatus)
+}
diff --git a/utils/password_concealer.go b/utils/password_concealer.go
index 0b4c4b9..985f3a7 100644
--- a/utils/password_concealer.go
+++ b/utils/password_concealer.go
@@ -2,72 +2,13 @@ package utils
import (
"bufio"
- "errors"
"fmt"
"io"
"os"
- "os/signal"
- "syscall"
- "unsafe"
- "golang.org/x/sys/unix"
+ "github.com/moooofly/harbor-go-client/utils/term"
)
-const (
- getTermios = unix.TCGETS
- setTermios = unix.TCSETS
-)
-
-var (
- // ErrInvalidState is returned if the state of the terminal is invalid.
- ErrInvalidState = errors.New("Invalid terminal state")
-)
-
-// Termios is the Unix API for terminal I/O.
-type Termios unix.Termios
-
-// State represents the state of the terminal.
-type State struct {
- termios Termios
-}
-
-func tcget(fd uintptr, p *Termios) syscall.Errno {
- _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(p)))
- return err
-}
-
-func tcset(fd uintptr, p *Termios) syscall.Errno {
- _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, setTermios, uintptr(unsafe.Pointer(p)))
- return err
-}
-
-// RestoreTerminal restores the terminal connected to the given file descriptor
-// to a previous state.
-func RestoreTerminal(fd uintptr, state *State) error {
- if state == nil {
- return ErrInvalidState
- }
- if err := tcset(fd, &state.termios); err != 0 {
- return err
- }
- return nil
-}
-
-func handleInterrupt(fd uintptr, state *State) {
- sigchan := make(chan os.Signal, 1)
- signal.Notify(sigchan, os.Interrupt)
- go func() {
- for range sigchan {
- // quit cleanly and the new terminal item is on a new line
- fmt.Println()
- signal.Stop(sigchan)
- close(sigchan)
- RestoreTerminal(fd, state)
- os.Exit(1)
- }
- }()
-}
-
func readInput(in io.Reader, out io.Writer) string {
reader := bufio.NewReader(in)
line, _, err := reader.ReadLine()
@@ -81,25 +22,23 @@ func readInput(in io.Reader, out io.Writer) string {
// ReadPasswordFromTerm gets user password from stdin without showing on screen
func ReadPasswordFromTerm() (string, error) {
- var oldState State
- if err := tcget(os.Stdin.Fd(), &oldState.termios); err != 0 {
+ oldState, err := term.SaveState(os.Stdin.Fd())
+ if err != nil {
return "", err
}
fmt.Fprintf(os.Stdout, "Password: ")
- newState := oldState.termios
- newState.Lflag &^= unix.ECHO
-
- if err := tcset(os.Stdin.Fd(), &newState); err != 0 {
+ err = term.DisableEcho(os.Stdin.Fd(), oldState)
+ if err != nil {
return "", err
}
- handleInterrupt(os.Stdin.Fd(), &oldState)
passwd := readInput(os.Stdin, os.Stdout)
fmt.Fprint(os.Stdout, "\n")
- if err := tcset(os.Stdin.Fd(), &oldState.termios); err != 0 {
+ err = term.RestoreTerminal(os.Stdin.Fd(), oldState)
+ if err != nil {
return "", err
}
diff --git a/utils/term/tc.go b/utils/term/tc.go
new file mode 100644
index 0000000..19dbb1c
--- /dev/null
+++ b/utils/term/tc.go
@@ -0,0 +1,20 @@
+// +build !windows
+
+package term
+
+import (
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+func tcget(fd uintptr, p *Termios) syscall.Errno {
+ _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(p)))
+ return err
+}
+
+func tcset(fd uintptr, p *Termios) syscall.Errno {
+ _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, setTermios, uintptr(unsafe.Pointer(p)))
+ return err
+}
diff --git a/utils/term/term.go b/utils/term/term.go
new file mode 100644
index 0000000..b0448fb
--- /dev/null
+++ b/utils/term/term.go
@@ -0,0 +1,74 @@
+// +build !windows
+
+// Package term provides structures and helper functions to work with
+// terminal (state, sizes).
+package term
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/signal"
+
+ "golang.org/x/sys/unix"
+)
+
+var (
+ // ErrInvalidState is returned if the state of the terminal is invalid.
+ ErrInvalidState = errors.New("Invalid terminal state")
+)
+
+// State represents the state of the terminal.
+type State struct {
+ termios Termios
+}
+
+// RestoreTerminal restores the terminal connected to the given file descriptor
+// to a previous state.
+func RestoreTerminal(fd uintptr, state *State) error {
+ if state == nil {
+ return ErrInvalidState
+ }
+ if err := tcset(fd, &state.termios); err != 0 {
+ return err
+ }
+ return nil
+}
+
+// SaveState saves the state of the terminal connected to the given file descriptor.
+func SaveState(fd uintptr) (*State, error) {
+ var oldState State
+ if err := tcget(fd, &oldState.termios); err != 0 {
+ return nil, err
+ }
+
+ return &oldState, nil
+}
+
+// DisableEcho applies the specified state to the terminal connected to the file
+// descriptor, with echo disabled.
+func DisableEcho(fd uintptr, state *State) error {
+ newState := state.termios
+ newState.Lflag &^= unix.ECHO
+
+ if err := tcset(fd, &newState); err != 0 {
+ return err
+ }
+ handleInterrupt(fd, state)
+ return nil
+}
+
+func handleInterrupt(fd uintptr, state *State) {
+ sigchan := make(chan os.Signal, 1)
+ signal.Notify(sigchan, os.Interrupt)
+ go func() {
+ for range sigchan {
+ // quit cleanly and the new terminal item is on a new line
+ fmt.Println()
+ signal.Stop(sigchan)
+ close(sigchan)
+ RestoreTerminal(fd, state)
+ os.Exit(1)
+ }
+ }()
+}
diff --git a/utils/term/termios_bsd.go b/utils/term/termios_bsd.go
new file mode 100644
index 0000000..42fd139
--- /dev/null
+++ b/utils/term/termios_bsd.go
@@ -0,0 +1,15 @@
+// +build darwin freebsd openbsd netbsd
+
+package term
+
+import (
+ "golang.org/x/sys/unix"
+)
+
+const (
+ getTermios = unix.TIOCGETA
+ setTermios = unix.TIOCSETA
+)
+
+// Termios is the Unix API for terminal I/O.
+type Termios unix.Termios
diff --git a/utils/term/termios_linux.go b/utils/term/termios_linux.go
new file mode 100644
index 0000000..bd7cdb9
--- /dev/null
+++ b/utils/term/termios_linux.go
@@ -0,0 +1,13 @@
+package term
+
+import (
+ "golang.org/x/sys/unix"
+)
+
+const (
+ getTermios = unix.TCGETS
+ setTermios = unix.TCSETS
+)
+
+// Termios is the Unix API for terminal I/O.
+type Termios unix.Termios