Repository: ceph/go-ceph
Pull request #160 (2020-01-24 13:49:22)

rados: add Create() function to create new objects
Ceph-CSI creates new objects without initial data. This is a functionality that is missing in go-ceph at the moment.

## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code


nixpanic: Add a special error for EEXISTS, and included it in the test case.

Please review again and merge if acceptable. Thanks!
phlogistonjohn: Sorry I goofed up and suggested "XXXErr" but the convention is actually "ErrXXX".  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming

Mea culpa.

Patch:
diff --git a/rados/ioctx.go b/rados/ioctx.go
index 76b4dd3..e5b9ada 100644
--- a/rados/ioctx.go
+++ b/rados/ioctx.go
@@ -30,6 +30,19 @@ import (
 	"unsafe"
 )
 
+// CreateOption is passed to IOContext.Create() and should be one of
+// CreateExclusive or CreateIdempotent.
+type CreateOption int
+
+const (
+	// CreateExclusive if used with IOContext.Create() and the object
+	// already exists, the function will return an error.
+	CreateExclusive = C.LIBRADOS_CREATE_EXCLUSIVE
+	// CreateIdempotent if used with IOContext.Create() and the object
+	// already exists, the function will not return an error.
+	CreateIdempotent = C.LIBRADOS_CREATE_IDEMPOTENT
+)
+
 // PoolStat represents Ceph pool statistics.
 type PoolStat struct {
 	// space used in bytes
@@ -93,6 +106,23 @@ func (ioctx *IOContext) SetNamespace(namespace string) {
 	C.rados_ioctx_set_namespace(ioctx.ioctx, c_ns)
 }
 
+// Create a new object with key oid.
+//
+// Implements:
+//  void rados_write_op_create(rados_write_op_t write_op, int exclusive,
+//                             const char* category)
+func (ioctx *IOContext) Create(oid string, exclusive CreateOption) error {
+	c_oid := C.CString(oid)
+	defer C.free(unsafe.Pointer(c_oid))
+
+	op := C.rados_create_write_op()
+	C.rados_write_op_create(op, C.int(exclusive), nil)
+	ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0)
+	C.rados_release_write_op(op)
+
+	return getRadosError(int(ret))
+}
+
 // Write writes len(data) bytes to the object with key oid starting at byte
 // offset offset. It returns an error, if any.
 func (ioctx *IOContext) Write(oid string, data []byte, offset uint64) error {
@@ -656,7 +686,7 @@ func (iter *Iter) Namespace() string {
 
 // Err checks whether the iterator has encountered an error.
 func (iter *Iter) Err() error {
-	if iter.err == RadosErrorNotFound {
+	if iter.err == ErrNotFound {
 		return nil
 	}
 	return iter.err
diff --git a/rados/rados.go b/rados/rados.go
index e23e8c0..2e4a487 100644
--- a/rados/rados.go
+++ b/rados/rados.go
@@ -26,14 +26,33 @@ func (e RadosError) Error() string {
 	return fmt.Sprintf("rados: ret=%d, %s", errno, s)
 }
 
-var (
-	// RadosAllNamespaces is used to reset a selected namespace to all namespaces. See the IOContext SetNamespace function.
-	RadosAllNamespaces = C.LIBRADOS_ALL_NSPACES
-
+const (
+	// AllNamespaces is used to reset a selected namespace to all
+	// namespaces. See the IOContext SetNamespace function.
+	AllNamespaces = C.LIBRADOS_ALL_NSPACES
+
+	// ErrNotFound indicates a missing resource.
+	ErrNotFound = RadosError(-C.ENOENT)
+	// ErrPermissionDenied indicates a permissions issue.
+	ErrPermissionDenied = RadosError(-C.EPERM)
+	// ErrObjectExists indicates that an exclusive object creation failed.
+	ErrObjectExists = RadosError(-C.EEXIST)
+
+	// FIXME: for backwards compatibility
+
+	// RadosAllNamespaces is used to reset a selected namespace to all
+	// namespaces. See the IOContext SetNamespace function.
+	//
+	// Deprecated: use AllNamespaces instead
+	RadosAllNamespaces = AllNamespaces
 	// RadosErrorNotFound indicates a missing resource.
-	RadosErrorNotFound = RadosError(-C.ENOENT)
+	//
+	// Deprecated: use ErrNotFound instead
+	RadosErrorNotFound = ErrNotFound
 	// RadosErrorPermissionDenied indicates a permissions issue.
-	RadosErrorPermissionDenied = RadosError(-C.EPERM)
+	//
+	// Deprecated: use ErrPermissionDenied instead
+	RadosErrorPermissionDenied = ErrPermissionDenied
 )
 
 func getRadosError(err int) error {


Test patch:
diff --git a/rados/rados_test.go b/rados/rados_test.go
index b8281ef..9af6afa 100644
--- a/rados/rados_test.go
+++ b/rados/rados_test.go
@@ -447,6 +447,20 @@ func (suite *RadosTestSuite) TestWaitForLatestOSDMap() {
 	assert.NoError(suite.T(), err)
 }
 
+func (suite *RadosTestSuite) TestCreate() {
+	suite.SetupConnection()
+
+	err := suite.ioctx.Create("unique", CreateExclusive)
+	assert.NoError(suite.T(), err)
+
+	err = suite.ioctx.Create("unique", CreateExclusive)
+	assert.Error(suite.T(), err)
+	assert.Equal(suite.T(), err, ErrObjectExists)
+
+	err = suite.ioctx.Create("unique", CreateIdempotent)
+	assert.NoError(suite.T(), err)
+}
+
 func (suite *RadosTestSuite) TestReadWrite() {
 	suite.SetupConnection()
 
@@ -500,7 +514,7 @@ func (suite *RadosTestSuite) TestReadNotFound() {
 	var bytes []byte
 	oid := suite.GenObjectName()
 	_, err := suite.ioctx.Read(oid, bytes, 0)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 }
 
 func (suite *RadosTestSuite) TestDeleteNotFound() {
@@ -508,7 +522,7 @@ func (suite *RadosTestSuite) TestDeleteNotFound() {
 
 	oid := suite.GenObjectName()
 	err := suite.ioctx.Delete(oid)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 }
 
 func (suite *RadosTestSuite) TestStatNotFound() {
@@ -516,7 +530,7 @@ func (suite *RadosTestSuite) TestStatNotFound() {
 
 	oid := suite.GenObjectName()
 	_, err := suite.ioctx.Stat(oid)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 }
 
 func (suite *RadosTestSuite) TestObjectStat() {
@@ -749,7 +763,7 @@ func (suite *RadosTestSuite) TestObjectIteratorAcrossNamespaces() {
 	objectListNS2 := []string{}
 
 	// populate list of current objects
-	suite.ioctx.SetNamespace(RadosAllNamespaces)
+	suite.ioctx.SetNamespace(AllNamespaces)
 	existingList := []string{}
 	iter, err := suite.ioctx.Iter()
 	assert.NoError(suite.T(), err)
@@ -782,7 +796,7 @@ func (suite *RadosTestSuite) TestObjectIteratorAcrossNamespaces() {
 	}
 	assert.True(suite.T(), len(createdList) == 20)
 
-	suite.ioctx.SetNamespace(RadosAllNamespaces)
+	suite.ioctx.SetNamespace(AllNamespaces)
 	iter, err = suite.ioctx.Iter()
 	assert.NoError(suite.T(), err)
 	rogueList := []string{}
@@ -1019,7 +1033,7 @@ func (suite *RadosTestSuite) TestSetNamespace() {
 	// oid isn't seen in space1 ns
 	suite.ioctx.SetNamespace("space1")
 	stat, err = suite.ioctx.Stat(oid)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 
 	// create oid2 in space1 ns
 	oid2 := suite.GenObjectName()
@@ -1029,7 +1043,7 @@ func (suite *RadosTestSuite) TestSetNamespace() {
 
 	suite.ioctx.SetNamespace("")
 	stat, err = suite.ioctx.Stat(oid2)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 
 	stat, err = suite.ioctx.Stat(oid)
 	assert.Equal(suite.T(), uint64(len(bytes_in)), stat.Size)
@@ -1067,7 +1081,7 @@ func (suite *RadosTestSuite) TestListAcrossNamespaces() {
 	assert.EqualValues(suite.T(), 1, nsFoundObjects)
 
 	// count objects in pool
-	suite.ioctx.SetNamespace(RadosAllNamespaces)
+	suite.ioctx.SetNamespace(AllNamespaces)
 	allFoundObjects := 0
 	err = suite.ioctx.ListObjects(func(oid string) {
 		allFoundObjects++
@@ -1170,7 +1184,7 @@ func (suite *RadosTestSuite) TestOmapOnNonexistentObjectError() {
 	suite.SetupConnection()
 	oid := suite.GenObjectName()
 	_, err := suite.ioctx.GetAllOmapValues(oid, "", "", 100)
-	assert.Equal(suite.T(), err, RadosErrorNotFound)
+	assert.Equal(suite.T(), err, ErrNotFound)
 }
 
 func (suite *RadosTestSuite) TestOpenIOContextInvalidPool() {