minio/cmd/gateway/manta/gateway-manta.go

596 lines
17 KiB
Go

/*
* Minio Cloud Storage, (C) 2017 Minio, 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 manta
import (
"context"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"github.com/hashicorp/errwrap"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
tclient "github.com/joyent/triton-go/client"
"github.com/joyent/triton-go/storage"
"github.com/minio/cli"
minio "github.com/minio/minio/cmd"
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/errors"
"github.com/minio/minio/pkg/hash"
)
// stor is a namespace within manta where you store any documents that are deemed as private
// and require access credentials to read them. Within the stor namespace, you can create any
// number of directories and objects.
const (
mantaBackend = "manta"
defaultMantaRoot = "/stor"
defaultMantaURL = "https://us-east.manta.joyent.com"
)
var mantaRoot = defaultMantaRoot
func init() {
const mantaGatewayTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}} [ENDPOINT]
{{if .VisibleFlags}}
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
ENDPOINT:
Manta server endpoint. Default ENDPOINT is https://us-east.manta.joyent.com
ENVIRONMENT VARIABLES:
ACCESS:
MINIO_ACCESS_KEY: The Manta account name.
MINIO_SECRET_KEY: A KeyID associated with the Manta account.
MANTA_KEY_MATERIAL: The path to the SSH Key associated with the Manta account if the MINIO_SECRET_KEY is not in SSH Agent.
MANTA_SUBUSER: The username of a user who has limited access to your account.
BROWSER:
MINIO_BROWSER: To disable web browser access, set this value to "off".
EXAMPLES:
1. Start minio gateway server for Manta Object Storage backend.
$ export MINIO_ACCESS_KEY=manta_account_name
$ export MINIO_SECRET_KEY=manta_key_id
$ {{.HelpName}}
2. Start minio gateway server for Manta Object Storage backend on custom endpoint.
$ export MINIO_ACCESS_KEY=manta_account_name
$ export MINIO_SECRET_KEY=manta_key_id
$ {{.HelpName}} https://us-west.manta.joyent.com
3. Start minio gateway server for Manta Object Storage backend without using SSH Agent.
$ export MINIO_ACCESS_KEY=manta_account_name
$ export MINIO_SECRET_KEY=manta_key_id
$ export MANTA_KEY_MATERIAL=~/.ssh/custom_rsa
$ {{.HelpName}}
`
minio.RegisterGatewayCommand(cli.Command{
Name: mantaBackend,
Usage: "Manta Object Storage.",
Action: mantaGatewayMain,
CustomHelpTemplate: mantaGatewayTemplate,
HideHelpCommand: true,
})
}
func mantaGatewayMain(ctx *cli.Context) {
// Validate gateway arguments.
host := ctx.Args().First()
// Validate gateway arguments.
minio.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), host), "Invalid argument")
minio.StartGateway(ctx, &Manta{host})
}
// Manta implements Gateway.
type Manta struct {
host string
}
// Name implements Gateway interface.
func (g *Manta) Name() string {
return mantaBackend
}
// NewGatewayLayer returns manta gateway layer, implements GatewayLayer interface to
// talk to manta remote backend.
func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.GatewayLayer, error) {
var err error
var signer authentication.Signer
var endpoint = defaultMantaURL
if g.host != "" {
endpoint, _, err = minio.ParseGatewayEndpoint(g.host)
if err != nil {
return nil, err
}
}
if overrideRoot, ok := os.LookupEnv("MANTA_ROOT"); ok {
mantaRoot = overrideRoot
}
keyMaterial := os.Getenv("MANTA_KEY_MATERIAL")
if keyMaterial == "" {
input := authentication.SSHAgentSignerInput{
KeyID: creds.SecretKey,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil {
return nil, errors.Trace(err)
}
} else {
var keyBytes []byte
if _, err = os.Stat(keyMaterial); err == nil {
keyBytes, err = ioutil.ReadFile(keyMaterial)
if err != nil {
return nil, fmt.Errorf("Error reading key material from %s: %s",
keyMaterial, err)
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, fmt.Errorf(
"Failed to read key material '%s': no key found", keyMaterial)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return nil, fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"+
"not currently supported. Please decrypt the key prior to use.", keyMaterial)
}
} else {
keyBytes = []byte(keyMaterial)
}
input := authentication.PrivateKeySignerInput{
KeyID: creds.SecretKey,
PrivateKeyMaterial: keyBytes,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil {
return nil, errors.Trace(err)
}
}
tc, err := storage.NewClient(&triton.ClientConfig{
MantaURL: endpoint,
AccountName: creds.AccessKey,
Signers: []authentication.Signer{signer},
})
if err != nil {
return nil, err
}
tc.Client.HTTPClient = &http.Client{
Transport: minio.NewCustomHTTPTransport(),
}
return &tritonObjects{
client: tc,
}, nil
}
// Production - Manta is not production ready.
func (g *Manta) Production() bool {
return false
}
// tritonObjects - Implements Object layer for Triton Manta storage
type tritonObjects struct {
minio.GatewayUnsupported
client *storage.StorageClient
}
// Shutdown - save any gateway metadata to disk
// if necessary and reload upon next restart.
func (t *tritonObjects) Shutdown() error {
return nil
}
// StorageInfo - Not relevant to Triton backend.
func (t *tritonObjects) StorageInfo() (si minio.StorageInfo) {
return si
}
//
// ~~~ Buckets ~~~
//
// MakeBucketWithLocation - Create a new directory within manta.
//
// https://apidocs.joyent.com/manta/api.html#PutDirectory
func (t *tritonObjects) MakeBucketWithLocation(bucket, location string) error {
ctx := context.Background()
err := t.client.Dir().Put(ctx, &storage.PutDirectoryInput{
DirectoryName: path.Join(mantaRoot, bucket),
})
if err != nil {
return err
}
return nil
}
// GetBucketInfo - Get directory metadata..
//
// https://apidocs.joyent.com/manta/api.html#GetObject
func (t *tritonObjects) GetBucketInfo(bucket string) (bi minio.BucketInfo, e error) {
var info minio.BucketInfo
ctx := context.Background()
resp, err := t.client.Objects().Get(ctx, &storage.GetObjectInput{
ObjectPath: path.Join(mantaRoot, bucket),
})
if err != nil {
return info, err
}
return minio.BucketInfo{
Name: bucket,
Created: resp.LastModified,
}, nil
}
// ListBuckets - Lists all Manta directories, uses Manta equivalent
// ListDirectories.
//
// https://apidocs.joyent.com/manta/api.html#ListDirectory
func (t *tritonObjects) ListBuckets() (buckets []minio.BucketInfo, err error) {
ctx := context.Background()
dirs, err := t.client.Dir().List(ctx, &storage.ListDirectoryInput{
DirectoryName: path.Join(mantaRoot),
})
if err != nil {
return nil, err
}
for _, dir := range dirs.Entries {
if dir.Type == "directory" {
buckets = append(buckets, minio.BucketInfo{
Name: dir.Name,
Created: dir.ModifiedTime,
})
}
}
return buckets, nil
}
// DeleteBucket - Delete a directory in Manta, uses Manta equivalent
// DeleteDirectory.
//
// https://apidocs.joyent.com/manta/api.html#DeleteDirectory
func (t *tritonObjects) DeleteBucket(bucket string) error {
ctx := context.Background()
return t.client.Dir().Delete(ctx, &storage.DeleteDirectoryInput{
DirectoryName: path.Join(mantaRoot, bucket),
})
}
//
// ~~~ Objects ~~~
//
// ListObjects - Lists all objects in Manta with a container filtered by prefix
// and marker, uses Manta equivalent ListDirectory.
//
// https://apidocs.joyent.com/manta/api.html#ListDirectory
func (t *tritonObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (result minio.ListObjectsInfo, err error) {
var (
dirName string
objs *storage.ListDirectoryOutput
input *storage.ListDirectoryInput
ctx = context.Background()
pathBase = path.Base(prefix)
)
// Make sure to only request a Dir.List for the parent "directory" for a
// given prefix first. We don't know if our prefix is referencing a
// directory or file name and can't send file names into Dir.List because
// that'll cause Manta to return file content in the response body. Dir.List
// expects to parse out directory entries in JSON. So, try the first
// directory name of the prefix path provided.
if pathDir := path.Dir(prefix); pathDir == "." {
dirName = path.Join(mantaRoot, bucket)
} else {
dirName = path.Join(mantaRoot, bucket, pathDir)
}
input = &storage.ListDirectoryInput{
DirectoryName: dirName,
Limit: uint64(maxKeys),
Marker: marker,
}
objs, err = t.client.Dir().List(ctx, input)
if err != nil {
if tclient.IsResourceNotFoundError(err) {
return result, nil
}
return result, errors.Trace(err)
}
for _, obj := range objs.Entries {
// If the base name of our prefix was found to be of type "directory"
// than we need to pull the directory entries for that instead.
if obj.Name == pathBase && obj.Type == "directory" {
input.DirectoryName = path.Join(mantaRoot, bucket, prefix)
objs, err = t.client.Dir().List(ctx, input)
if err != nil {
return result, errors.Trace(err)
}
break
}
}
isTruncated := true // Always send a second request.
if marker == "" && len(objs.Entries) < maxKeys {
isTruncated = false
} else if marker != "" && len(objs.Entries) < maxKeys {
isTruncated = false
}
for _, obj := range objs.Entries {
if obj.Type == "directory" {
result.Prefixes = append(result.Prefixes, obj.Name+delimiter)
} else {
result.Objects = append(result.Objects, minio.ObjectInfo{
Name: obj.Name,
Size: int64(obj.Size),
ModTime: obj.ModifiedTime,
ETag: obj.ETag,
})
}
}
result.IsTruncated = isTruncated
if isTruncated {
result.NextMarker = result.Objects[len(result.Objects)-1].Name
}
return result, nil
}
//
// ~~~ Objects ~~~
//
// ListObjectsV2 - Lists all objects in Manta with a container filtered by prefix
// and continuationToken, uses Manta equivalent ListDirectory.
//
// https://apidocs.joyent.com/manta/api.html#ListDirectory
func (t *tritonObjects) ListObjectsV2(bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) {
var (
dirName string
objs *storage.ListDirectoryOutput
input *storage.ListDirectoryInput
ctx = context.Background()
pathBase = path.Base(prefix)
)
if pathDir := path.Dir(prefix); pathDir == "." {
dirName = path.Join(mantaRoot, bucket)
} else {
dirName = path.Join(mantaRoot, bucket, pathDir)
}
input = &storage.ListDirectoryInput{
DirectoryName: dirName,
Limit: uint64(maxKeys),
Marker: continuationToken,
}
objs, err = t.client.Dir().List(ctx, input)
if err != nil {
if tclient.IsResourceNotFoundError(err) {
return result, nil
}
return result, errors.Trace(err)
}
for _, obj := range objs.Entries {
if obj.Name == pathBase && obj.Type == "directory" {
input.DirectoryName = path.Join(mantaRoot, bucket, prefix)
objs, err = t.client.Dir().List(ctx, input)
if err != nil {
return result, errors.Trace(err)
}
break
}
}
isTruncated := true // Always send a second request.
if continuationToken == "" && len(objs.Entries) < maxKeys {
isTruncated = false
} else if continuationToken != "" && len(objs.Entries) < maxKeys {
isTruncated = false
}
for _, obj := range objs.Entries {
if obj.Type == "directory" {
result.Prefixes = append(result.Prefixes, obj.Name+delimiter)
} else {
result.Objects = append(result.Objects, minio.ObjectInfo{
Name: obj.Name,
Size: int64(obj.Size),
ModTime: obj.ModifiedTime,
ETag: obj.ETag,
})
}
}
result.IsTruncated = isTruncated
if isTruncated {
result.NextContinuationToken = result.Objects[len(result.Objects)-1].Name
}
return result, nil
}
// GetObject - Reads an object from Manta. Supports additional parameters like
// offset and length which are synonymous with HTTP Range requests.
//
// startOffset indicates the starting read location of the object. length
// indicates the total length of the object.
//
// https://apidocs.joyent.com/manta/api.html#GetObject
func (t *tritonObjects) GetObject(bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
// Start offset cannot be negative.
if startOffset < 0 {
return errors.Trace(fmt.Errorf("Unexpected error"))
}
ctx := context.Background()
output, err := t.client.Objects().Get(ctx, &storage.GetObjectInput{
ObjectPath: path.Join(mantaRoot, bucket, object),
})
if err != nil {
return err
}
defer output.ObjectReader.Close()
// Read until startOffset and discard, Manta object storage doesn't support range GET requests yet.
if _, err = io.CopyN(ioutil.Discard, output.ObjectReader, startOffset); err != nil {
return err
}
if length > 0 {
_, err = io.Copy(writer, io.LimitReader(output.ObjectReader, length))
} else {
_, err = io.Copy(writer, output.ObjectReader)
}
return err
}
// GetObjectInfo - reads blob metadata properties and replies back minio.ObjectInfo,
// uses Triton equivalent GetBlobProperties.
//
// https://apidocs.joyent.com/manta/api.html#GetObject
func (t *tritonObjects) GetObjectInfo(bucket, object string) (objInfo minio.ObjectInfo, err error) {
ctx := context.Background()
info, err := t.client.Objects().GetInfo(ctx, &storage.GetInfoInput{
ObjectPath: path.Join(mantaRoot, bucket, object),
})
if err != nil {
errType := &tclient.MantaError{}
if errwrap.ContainsType(err, errType) {
mantaErr := errwrap.GetType(err, errType).(*tclient.MantaError)
if mantaErr.StatusCode == http.StatusNotFound {
return objInfo, minio.ObjectNotFound{
Bucket: bucket,
Object: object,
}
}
}
return objInfo, err
}
return minio.ObjectInfo{
Bucket: bucket,
ContentType: info.ContentType,
Size: int64(info.ContentLength),
ETag: info.ETag,
ModTime: info.LastModified,
UserDefined: info.Metadata,
IsDir: strings.HasSuffix(info.ContentType, "type=directory"),
}, nil
}
type dummySeeker struct {
io.Reader
}
func (d dummySeeker) Seek(offset int64, whence int) (int64, error) {
return 0, nil
}
// PutObject - Create a new blob with the incoming data, uses Triton equivalent
// CreateBlockBlobFromReader.
//
// https://apidocs.joyent.com/manta/api.html#PutObject
func (t *tritonObjects) PutObject(bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
ctx := context.Background()
if err = t.client.Objects().Put(ctx, &storage.PutObjectInput{
ContentLength: uint64(data.Size()),
ObjectPath: path.Join(mantaRoot, bucket, object),
ContentType: metadata["content-type"],
// TODO: Change to `string(data.md5sum)` if/when that becomes an exported field
ContentMD5: metadata["content-md5"],
ObjectReader: dummySeeker{data},
ForceInsert: true,
}); err != nil {
return objInfo, errors.Trace(err)
}
if err = data.Verify(); err != nil {
t.DeleteObject(bucket, object)
return objInfo, errors.Trace(err)
}
return t.GetObjectInfo(bucket, object)
}
// CopyObject - Copies a blob from source container to destination container.
// Uses Manta Snaplinks API.
//
// https://apidocs.joyent.com/manta/api.html#PutSnapLink
func (t *tritonObjects) CopyObject(srcBucket, srcObject, destBucket, destObject string, metadata map[string]string, srcEtag string) (objInfo minio.ObjectInfo, err error) {
ctx := context.Background()
if err = t.client.SnapLinks().Put(ctx, &storage.PutSnapLinkInput{
SourcePath: path.Join(mantaRoot, srcBucket, srcObject),
LinkPath: path.Join(mantaRoot, destBucket, destObject),
}); err != nil {
return objInfo, errors.Trace(err)
}
return t.GetObjectInfo(destBucket, destObject)
}
// DeleteObject - Delete a blob in Manta, uses Triton equivalent DeleteBlob API.
//
// https://apidocs.joyent.com/manta/api.html#DeleteObject
func (t *tritonObjects) DeleteObject(bucket, object string) error {
ctx := context.Background()
if err := t.client.Objects().Delete(ctx, &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaRoot, bucket, object),
}); err != nil {
return errors.Trace(err)
}
return nil
}