update storageclass-accessor webhook (#5927)

Signed-off-by: stoneshi-yunify <stoneshi@kubesphere.io>
This commit is contained in:
yonghongshi
2023-10-07 10:45:53 +08:00
committed by GitHub
parent 925f3091f8
commit b0812d183d
14 changed files with 227 additions and 648 deletions

View File

@@ -92,7 +92,7 @@ func (cw *CertWatcher) Watch() {
return
}
klog.Error(err, "certificate watch error")
klog.ErrorS(err, "certificate watch failed")
}
}
}
@@ -126,12 +126,12 @@ func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
// If the file was removed, re-add the watch.
if isRemove(event) {
if err := cw.watcher.Add(event.Name); err != nil {
klog.Error(err, "error re-watching file")
klog.ErrorS(err, "failed to re-watching file")
}
}
if err := cw.ReadCertificate(); err != nil {
klog.Error(err, "error re-reading certificate")
klog.ErrorS(err, "failed to re-reading certificate")
}
}

View File

@@ -1,10 +1,16 @@
package webhook
import (
"context"
"fmt"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
type ReqInfo struct {
@@ -20,28 +26,55 @@ var reviewResponse = &admissionv1.AdmissionResponse{
Result: &metav1.Status{},
}
func AdmitPVC(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
klog.Info("admitting pvc")
type Admitter struct {
client client.Client
}
func NewAdmitter() (*Admitter, error) {
cfg, err := config.GetConfig()
if err != nil {
return nil, err
}
cli, err := client.New(cfg, client.Options{
Scheme: scheme,
})
if err != nil {
return nil, err
}
a := &Admitter{
client: cli,
}
return a, nil
}
func NewAdmitterWithClient(client client.Client) *Admitter {
return &Admitter{
client: client,
}
}
func (a *Admitter) serverPVCRequest(w http.ResponseWriter, r *http.Request) {
server(w, r, newDelegateToV1AdmitHandler(a.AdmitPVC))
}
func (a *Admitter) AdmitPVC(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
if ar.Request.Operation != admissionv1.Create {
return reviewResponse
}
raw := ar.Request.Object.Raw
var newPVC *corev1.PersistentVolumeClaim
deserializer := codecs.UniversalDeserializer()
pvc := &corev1.PersistentVolumeClaim{}
obj, _, err := deserializer.Decode(raw, nil, pvc)
if err != nil {
klog.Error(err)
klog.ErrorS(err, "failed to decode raw object")
return toV1AdmissionResponse(err)
}
var ok bool
newPVC, ok = obj.(*corev1.PersistentVolumeClaim)
newPVC, ok := obj.(*corev1.PersistentVolumeClaim)
if !ok {
klog.Error("obj can't exchange to pvc object")
err = fmt.Errorf("obj can't exchange to pvc object")
klog.ErrorS(err, "failed to exchange to pvc object")
return toV1AdmissionResponse(err)
}
@@ -52,26 +85,28 @@ func AdmitPVC(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
operator: string(ar.Request.Operation),
storageClassName: *newPVC.Spec.StorageClassName,
}
return DecidePVCV1(reqPVC)
klog.Infof("request pvc: %v", reqPVC)
return a.decidePVCV1(context.Background(), reqPVC)
}
func DecidePVCV1(pvc ReqInfo) *admissionv1.AdmissionResponse {
accessors, err := getAccessors(pvc.storageClassName)
func (a *Admitter) decidePVCV1(ctx context.Context, pvc ReqInfo) *admissionv1.AdmissionResponse {
accessors, err := a.getAccessors(ctx, pvc.storageClassName)
if err != nil {
klog.Error("get accessor failed, err:", err)
klog.ErrorS(err, "get accessor failed")
return toV1AdmissionResponse(err)
} else if len(accessors) == 0 {
klog.Info("Not Found accessor for the storageClass:", pvc.storageClassName)
klog.Infof("Not Found accessor for the storageClass: %s", pvc.storageClassName)
return reviewResponse
}
for _, accessor := range accessors {
if err = validateNameSpace(pvc, accessor); err != nil {
klog.Infof("starting validating accessor: %s", accessor.Name)
if err = a.validateNameSpace(ctx, pvc, &accessor); err != nil {
return toV1AdmissionResponse(err)
}
if err = validateWorkSpace(pvc, accessor); err != nil {
if err = a.validateWorkSpace(ctx, pvc, &accessor); err != nil {
return toV1AdmissionResponse(err)
}
}

View File

@@ -1,6 +1,7 @@
package webhook
import (
"github.com/kubesphere/storageclass-accessor/client/apis/accessor/v1alpha1"
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
@@ -9,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
workspacev1alpha1 "kubesphere.io/api/tenant/v1alpha1"
)
var scheme = runtime.NewScheme()
@@ -24,4 +26,6 @@ func addToScheme(scheme *runtime.Scheme) {
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionv1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(workspacev1alpha1.AddToScheme(scheme))
}

View File

@@ -1,9 +1,11 @@
package webhook
import (
"github.com/kubesphere/storageclass-accessor/client/apis/accessor/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/utils/strings/slices"
workspacev1alpha1 "kubesphere.io/api/tenant/v1alpha1"
"github.com/kubesphere/storageclass-accessor/client/apis/accessor/v1alpha1"
)
func matchLabel(info map[string]string, expressions []v1alpha1.MatchExpressions) bool {
@@ -14,11 +16,26 @@ func matchLabel(info map[string]string, expressions []v1alpha1.MatchExpressions)
for _, rule := range expressions {
rulePass := true
for _, item := range rule.MatchExpressions {
if len(item.Values) == 0 {
continue
}
var labelKeyExist bool
_, ok := info[item.Key]
if ok {
labelKeyExist = true
}
switch item.Operator {
case v1alpha1.In:
rulePass = rulePass && inList(info[item.Key], item.Values)
rulePass = rulePass && labelKeyExist && (slices.Contains(item.Values, "*") || slices.Contains(item.Values, info[item.Key]))
case v1alpha1.NotIn:
rulePass = rulePass && !inList(info[item.Key], item.Values)
if labelKeyExist {
rulePass = rulePass && !slices.Contains(item.Values, "*") && !slices.Contains(item.Values, info[item.Key])
}
default:
continue
}
if !rulePass {
break
}
}
if rulePass {
@@ -28,7 +45,7 @@ func matchLabel(info map[string]string, expressions []v1alpha1.MatchExpressions)
return false
}
func matchField(ns *corev1.Namespace, expressions []v1alpha1.FieldExpressions) bool {
func nsMatchField(ns *corev1.Namespace, expressions []v1alpha1.FieldExpressions) bool {
//If not set limit, default pass
if len(expressions) == 0 {
return true
@@ -37,18 +54,28 @@ func matchField(ns *corev1.Namespace, expressions []v1alpha1.FieldExpressions) b
for _, rule := range expressions {
rulePass := true
for _, item := range rule.FieldExpressions {
if len(item.Values) == 0 {
continue
}
var val string
switch item.Field {
case v1alpha1.Name:
val = ns.Name
case v1alpha1.Status:
val = string(ns.Status.Phase)
default:
continue
}
switch item.Operator {
case v1alpha1.In:
rulePass = rulePass && inList(val, item.Values)
rulePass = rulePass && (slices.Contains(item.Values, "*") || slices.Contains(item.Values, val))
case v1alpha1.NotIn:
rulePass = rulePass && !inList(val, item.Values)
rulePass = rulePass && !slices.Contains(item.Values, "*") && !slices.Contains(item.Values, val)
default:
continue
}
if !rulePass {
break
}
}
if rulePass {
@@ -66,15 +93,29 @@ func wsMatchField(ws *workspacev1alpha1.Workspace, expressions []v1alpha1.FieldE
for _, rule := range expressions {
pass := true
for _, item := range rule.FieldExpressions {
if len(item.Values) == 0 {
continue
}
var val string
switch item.Field {
case v1alpha1.Name:
val = ws.Name
case v1alpha1.Status:
// TODO(stone): check status
continue
default:
continue
}
switch item.Operator {
case v1alpha1.In:
pass = pass && inList(ws.Name, item.Values)
pass = pass && (slices.Contains(item.Values, "*") || slices.Contains(item.Values, val))
case v1alpha1.NotIn:
pass = pass && !inList(ws.Name, item.Values)
pass = pass && !slices.Contains(item.Values, "*") && !slices.Contains(item.Values, val)
default:
continue
}
if !pass {
break
}
}
if pass {
@@ -83,12 +124,3 @@ func wsMatchField(ws *workspacev1alpha1.Workspace, expressions []v1alpha1.FieldE
}
return false
}
func inList(val string, list []string) bool {
for _, elements := range list {
if val == elements {
return true
}
}
return false
}

View File

@@ -3,127 +3,87 @@ package webhook
import (
"context"
"fmt"
"github.com/kubesphere/storageclass-accessor/client/apis/accessor/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
workspacev1alpha1 "kubesphere.io/api/tenant/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
func validateNameSpace(reqResource ReqInfo, accessor *v1alpha1.Accessor) error {
klog.Info("start validate namespace")
//accessor, err := getAccessor()
ns, err := getNameSpace(reqResource.namespace)
func (a *Admitter) validateNameSpace(ctx context.Context, reqResource ReqInfo, accessor *v1alpha1.Accessor) error {
klog.Infof("start validating namespace: %s", reqResource.namespace)
ns, err := a.getNameSpace(ctx, reqResource.namespace)
if err != nil {
klog.Error(err)
klog.ErrorS(err, "get namespace failed", "namespace", reqResource.namespace)
return err
}
var fieldPass, labelPass bool
fieldPass = matchField(ns, accessor.Spec.NameSpaceSelector.FieldSelector)
labelPass = matchLabel(ns.Labels, accessor.Spec.NameSpaceSelector.LabelSelector)
fieldPass := nsMatchField(ns, accessor.Spec.NameSpaceSelector.FieldSelector)
labelPass := matchLabel(ns.Labels, accessor.Spec.NameSpaceSelector.LabelSelector)
if fieldPass && labelPass {
return nil
}
klog.Error(fmt.Sprintf("%s %s does not allowed %s in the namespace: %s", reqResource.resource, reqResource.name, reqResource.operator, reqResource.namespace))
return fmt.Errorf("The storageClass: %s does not allowed %s %s %s in the namespace: %s ", reqResource.storageClassName, reqResource.operator, reqResource.resource, reqResource.name, reqResource.namespace)
err = fmt.Errorf("%s %s is not allowed %s in the namespace: %s", reqResource.resource, reqResource.name, reqResource.operator, reqResource.namespace)
klog.ErrorS(err, "validate namespace failed")
return err
}
func validateWorkSpace(reqResource ReqInfo, accessor *v1alpha1.Accessor) error {
klog.Info("start validate workspace")
func (a *Admitter) validateWorkSpace(ctx context.Context, reqResource ReqInfo, accessor *v1alpha1.Accessor) error {
klog.Infof("start validating workspace for namespace: %s", reqResource.namespace)
ns, err := getNameSpace(reqResource.namespace)
ns, err := a.getNameSpace(ctx, reqResource.namespace)
if err != nil {
klog.Error(err)
klog.ErrorS(err, "get namespace failed", "namespace", reqResource.namespace)
return err
}
if wsName, ok := ns.Labels["kubesphere.io/workspace"]; ok {
klog.Infof("namespace %s is in workspace %s", reqResource.namespace, wsName)
var ws *workspacev1alpha1.Workspace
ws, err = getWorkSpace(wsName)
ws, err = a.getWorkSpace(ctx, wsName)
if err != nil {
klog.Error("Cannot get the workspace")
klog.ErrorS(err, "failed to get the workspace", "workspace", wsName)
return err
}
var fieldPass, labelPass bool
fieldPass = wsMatchField(ws, accessor.Spec.WorkSpaceSelector.FieldSelector)
labelPass = matchLabel(ns.Labels, accessor.Spec.WorkSpaceSelector.LabelSelector)
fieldPass := wsMatchField(ws, accessor.Spec.WorkSpaceSelector.FieldSelector)
labelPass := matchLabel(ws.Labels, accessor.Spec.WorkSpaceSelector.LabelSelector)
if fieldPass && labelPass {
return nil
}
klog.Error(fmt.Sprintf("%s %s does not allowed %s in the workspace: %s", reqResource.resource, reqResource.name, reqResource.operator, wsName))
return fmt.Errorf("The storageClass: %s does not allowed %s %s %s in the workspace: %s ", reqResource.storageClassName, reqResource.operator, reqResource.resource, reqResource.name, wsName)
err = fmt.Errorf("%s %s is not allowed %s in the workspace: %s", reqResource.resource, reqResource.name, reqResource.operator, wsName)
klog.ErrorS(err, "validate workspace failed", "workspace", wsName)
return err
}
klog.Info("Unable to get workspace information, skipped.")
klog.Infof("namespace %s has no workspace information, skipped", reqResource.namespace)
return nil
}
func getNameSpace(nameSpaceName string) (*corev1.Namespace, error) {
nsClient, err := client.New(config.GetConfigOrDie(), client.Options{})
if err != nil {
return nil, err
}
func (a *Admitter) getNameSpace(ctx context.Context, nameSpaceName string) (*corev1.Namespace, error) {
ns := &corev1.Namespace{}
err = nsClient.Get(context.Background(), types.NamespacedName{Namespace: "", Name: nameSpaceName}, ns)
if err != nil {
klog.Error("client get namespace failed, err:", err)
return nil, err
}
return ns, nil
err := a.client.Get(ctx, types.NamespacedName{Name: nameSpaceName}, ns)
return ns, err
}
func getAccessors(storageClassName string) ([]*v1alpha1.Accessor, error) {
// get config
cfg, err := config.GetConfig()
if err != nil {
return nil, err
}
var cli client.Client
opts := client.Options{}
scheme := runtime.NewScheme()
_ = v1alpha1.AddToScheme(scheme)
opts.Scheme = scheme
cli, err = client.New(cfg, opts)
if err != nil {
return nil, err
}
func (a *Admitter) getAccessors(ctx context.Context, storageClassName string) ([]v1alpha1.Accessor, error) {
accessorList := &v1alpha1.AccessorList{}
var listOpt []client.ListOption
err = cli.List(context.Background(), accessorList, listOpt...)
err := a.client.List(ctx, accessorList)
if err != nil {
klog.ErrorS(err, "failed to list accessors for storage class", "storageClassName", storageClassName)
// TODO If not found , pass or not?
return nil, err
}
list := make([]*v1alpha1.Accessor, 0)
list := make([]v1alpha1.Accessor, 0)
for _, accessor := range accessorList.Items {
if accessor.Spec.StorageClassName == storageClassName {
list = append(list, &accessor)
list = append(list, accessor)
}
}
return list, nil
}
func getWorkSpace(workspaceName string) (*workspacev1alpha1.Workspace, error) {
cfg, err := config.GetConfig()
if err != nil {
return nil, err
}
var cli client.Client
opts := client.Options{}
scheme := runtime.NewScheme()
_ = workspacev1alpha1.AddToScheme(scheme)
opts.Scheme = scheme
cli, err = client.New(cfg, opts)
if err != nil {
return nil, err
}
func (a *Admitter) getWorkSpace(ctx context.Context, workspaceName string) (*workspacev1alpha1.Workspace, error) {
workspace := &workspacev1alpha1.Workspace{}
err = cli.Get(context.Background(), types.NamespacedName{Namespace: "", Name: workspaceName}, workspace)
if err != nil {
klog.Error("can't get the workspace by name, err:", err)
}
err := a.client.Get(ctx, types.NamespacedName{Name: workspaceName}, workspace)
return workspace, err
}

View File

@@ -5,12 +5,14 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/spf13/cobra"
"io/ioutil"
v1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
"net/http"
)
var (
@@ -50,38 +52,39 @@ func newDelegateToV1AdmitHandler(f admitV1Func) admitHandler {
}
func server(w http.ResponseWriter, r *http.Request, admit admitHandler) {
var body []byte
var err error
if r.Body == nil {
msg := "Expected request body to be non-empty"
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
err = fmt.Errorf("request body is nil")
klog.ErrorS(err, "request body is nil")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data, err := ioutil.ReadAll(r.Body)
var body []byte
body, err = io.ReadAll(r.Body)
if err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
klog.ErrorS(err, "read request body failed")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
body = data
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
msg := fmt.Sprintf("contentType=%s, expect application/json", contentType)
klog.Errorf(msg)
http.Error(w, msg, http.StatusBadRequest)
err = fmt.Errorf("contentType=%s, expect application/json", contentType)
klog.ErrorS(err, "contentType is not application/json")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
klog.V(2).Info(fmt.Sprintf("handling request: %s", body))
klog.Infof("handling request: %s", body)
deserializer := codecs.UniversalDeserializer()
obj, gvk, err := deserializer.Decode(body, nil, nil)
var obj runtime.Object
var gvk *schema.GroupVersionKind
obj, gvk, err = codecs.UniversalDeserializer().Decode(body, nil, nil)
if err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
klog.ErrorS(err, "request body could not be decoded")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -91,9 +94,9 @@ func server(w http.ResponseWriter, r *http.Request, admit admitHandler) {
case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
requestedAdmissionReview, ok := obj.(*v1.AdmissionReview)
if !ok {
msg := fmt.Sprintf("Expected v1.AdmissionReview but got: %T", obj)
klog.Errorf(msg)
http.Error(w, msg, http.StatusBadRequest)
err = fmt.Errorf("expected v1.AdmissionReview but got: %T", obj)
klog.ErrorS(err, "wrong object type")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
responseAdmissionReview := &v1.AdmissionReview{}
@@ -101,41 +104,40 @@ func server(w http.ResponseWriter, r *http.Request, admit admitHandler) {
responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview)
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
responseObj = responseAdmissionReview
klog.Infof("start writing response: %v", responseObj)
var respBytes []byte
respBytes, err = json.Marshal(responseObj)
if err != nil {
klog.ErrorS(err, "failed to marshal response object")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(respBytes)
if err != nil {
klog.ErrorS(err, "failed to write response")
}
default:
msg := fmt.Sprintf("Unsupported group version kind: %v", gvk)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
err = fmt.Errorf("unsupported group version kind: %v", gvk)
klog.ErrorS(err, "unsupported group version kind")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
klog.V(2).Info(fmt.Sprintf("sending response: %v", responseObj))
respBytes, err := json.Marshal(responseObj)
if err != nil {
klog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
return
}
func serverPVCRequest(w http.ResponseWriter, r *http.Request) {
server(w, r, newDelegateToV1AdmitHandler(AdmitPVC))
}
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher) error {
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher, admitter *Admitter) error {
go func() {
klog.Info("Starting certificate watcher")
if err := cw.Start(ctx); err != nil {
klog.Errorf("certificate watcher error: %v", err)
klog.ErrorS(err, "failed to start certificate watcher")
}
}()
mux := http.NewServeMux()
mux.HandleFunc("/persistentvolumeclaims", serverPVCRequest)
mux.HandleFunc("/persistentvolumeclaims", admitter.serverPVCRequest)
srv := &http.Server{
Handler: mux,
TLSConfig: tlsConfig,
@@ -148,6 +150,7 @@ func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher) er
}
return srv.Serve(listener)
}
func main(cmd *cobra.Command, args []string) {
// Create new cert watcher
ctx, cancel := context.WithCancel(cmd.Context())
@@ -160,7 +163,13 @@ func main(cmd *cobra.Command, args []string) {
GetCertificate: cw.GetCertificate,
}
if err := startServer(ctx, tslConfig, cw); err != nil {
klog.Fatalf("server stopped: %v", err)
admitter, err := NewAdmitter()
if err != nil {
klog.Fatalf("failed to initialize new admitter: %v", err)
}
err = startServer(ctx, tslConfig, cw, admitter)
if err != nil {
klog.Fatalf("failed to start server: %v", err)
}
}

3
vendor/modules.txt vendored
View File

@@ -615,7 +615,7 @@ github.com/kubesphere/pvc-autoresizer/runners
# github.com/kubesphere/sonargo v0.0.2 => github.com/kubesphere/sonargo v0.0.2
## explicit
github.com/kubesphere/sonargo/sonar
# github.com/kubesphere/storageclass-accessor v0.2.2
# github.com/kubesphere/storageclass-accessor v0.2.4-0.20230919084454-2f39c69db301 => github.com/kubesphere/storageclass-accessor v0.2.4-0.20230919084454-2f39c69db301
## explicit; go 1.16
github.com/kubesphere/storageclass-accessor/client/apis/accessor/v1alpha1
github.com/kubesphere/storageclass-accessor/webhook
@@ -2711,6 +2711,7 @@ sigs.k8s.io/yaml
# github.com/kubernetes-csi/external-snapshotter/client/v4 => github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0
# github.com/kubesphere/pvc-autoresizer => github.com/kubesphere/pvc-autoresizer v0.3.0
# github.com/kubesphere/sonargo => github.com/kubesphere/sonargo v0.0.2
# github.com/kubesphere/storageclass-accessor => github.com/kubesphere/storageclass-accessor v0.2.4-0.20230919084454-2f39c69db301
# github.com/lann/builder => github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
# github.com/lann/ps => github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0
# github.com/lib/pq => github.com/lib/pq v1.10.7