Пример #1
0
func checkFleetVersion() {
	output, err := utils.ExecCmdGetOutput("fleetctl")
	if err != nil {
		log.Fatal("fleetctl is required in PATH")
	}

	scanner := bufio.NewScanner(strings.NewReader(output))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.TrimSpace(line) == "VERSION:" {
			scanner.Scan()
			versionString := strings.TrimSpace(scanner.Text())
			version, err := semver.NewVersion(versionString)
			if err != nil {
				log.Error("Cannot parse version of fleetctl", versionString)
				os.Exit(1)
			}
			supported, _ := semver.NewVersion(FLEET_SUPPORTED_VERSION)
			if version.LessThan(*supported) {
				log.Error("fleetctl version in your path is too old. Require >= " + FLEET_SUPPORTED_VERSION)
				os.Exit(1)
			}
			break
		}
	}
}
Пример #2
0
// CanUseIptablesProxier returns true if we should use the iptables Proxier
// instead of the "classic" userspace Proxier.  This is determined by checking
// the iptables version and for the existence of kernel features. It may return
// an error if it fails to get the iptables version without error, in which
// case it will also return false.
func CanUseIptablesProxier(iptver IptablesVersioner) (bool, error) {
	minVersion, err := semver.NewVersion(iptablesMinVersion)
	if err != nil {
		return false, err
	}
	// returns "X.Y.Z"
	versionString, err := iptver.GetVersion()
	if err != nil {
		return false, err
	}
	version, err := semver.NewVersion(versionString)
	if err != nil {
		return false, err
	}
	if version.LessThan(*minVersion) {
		return false, nil
	}

	// Check for the required sysctls.  We don't care about the value, just
	// that it exists.  If this Proxier is chosen, we'll iniialize it as we
	// need.
	// TODO: we should inject a sysctl.Interface like we do for iptables
	_, err = utilsysctl.GetSysctl(sysctlRouteLocalnet)
	if err != nil {
		return false, err
	}

	return true, nil
}
Пример #3
0
// decideClusterVersion decides the cluster version based on the versions map.
// The returned version is the min server version in the map, or nil if the min
// version in unknown.
func decideClusterVersion(vers map[string]*version.Versions) *semver.Version {
	var cv *semver.Version
	lv := semver.Must(semver.NewVersion(version.Version))

	for mid, ver := range vers {
		if ver == nil {
			return nil
		}
		v, err := semver.NewVersion(ver.Server)
		if err != nil {
			plog.Errorf("cannot understand the version of member %s (%v)", mid, err)
			return nil
		}
		if lv.LessThan(*v) {
			plog.Warningf("the local etcd version %s is not up-to-date", lv.String())
			plog.Warningf("member %s has a higher version %s", mid, ver.Server)
		}
		if cv == nil {
			cv = v
		} else if v.LessThan(*cv) {
			cv = v
		}
	}
	return cv
}
Пример #4
0
func checkRktVersion() {
	output, err := utils.ExecCmdGetOutput("rkt")
	if err != nil {
		panic("rkt is required in PATH")
	}

	scanner := bufio.NewScanner(strings.NewReader(output))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.TrimSpace(line) == "VERSION:" {
			scanner.Scan()
			versionString := strings.TrimSpace(scanner.Text())
			version, err := semver.NewVersion(versionString)
			if err != nil {
				panic("Cannot parse version of rkt" + versionString)
			}
			supported, _ := semver.NewVersion(RKT_SUPPORTED_VERSION)
			if version.LessThan(*supported) {
				panic("rkt version in your path is too old. Require >= " + RKT_SUPPORTED_VERSION)
			}
			break
		}
	}

}
Пример #5
0
// Checks if iptables version has a "wait" flag
func getIptablesWaitFlag(vstring string) []string {
	version, err := semver.NewVersion(vstring)
	if err != nil {
		glog.Errorf("vstring (%s) is not a valid version string: %v", vstring, err)
		return nil
	}

	minVersion, err := semver.NewVersion(MinWaitVersion)
	if err != nil {
		glog.Errorf("MinWaitVersion (%s) is not a valid version string: %v", MinWaitVersion, err)
		return nil
	}
	if version.LessThan(*minVersion) {
		return nil
	}

	minVersion, err = semver.NewVersion(MinWait2Version)
	if err != nil {
		glog.Errorf("MinWait2Version (%s) is not a valid version string: %v", MinWait2Version, err)
		return nil
	}
	if version.LessThan(*minVersion) {
		return []string{"-w"}
	} else {
		return []string{"-w2"}
	}
}
Пример #6
0
func TestServerVersion(t *testing.T) {
	tests := []struct {
		h  http.Header
		wv *semver.Version
	}{
		// backward compatibility with etcd 2.0
		{
			http.Header{},
			semver.Must(semver.NewVersion("2.0.0")),
		},
		{
			http.Header{"X-Server-Version": []string{"2.1.0"}},
			semver.Must(semver.NewVersion("2.1.0")),
		},
		{
			http.Header{"X-Server-Version": []string{"2.1.0-alpha.0+git"}},
			semver.Must(semver.NewVersion("2.1.0-alpha.0+git")),
		},
	}
	for i, tt := range tests {
		v := serverVersion(tt.h)
		if v.String() != tt.wv.String() {
			t.Errorf("#%d: version = %s, want %s", i, v, tt.wv)
		}
	}
}
Пример #7
0
func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {

	minVersion, err := semver.NewVersion(mw.MinVersion)
	if err != nil {
		panic(err)
	}

	maxVersion, err := semver.NewVersion(mw.MaxVersion)
	if err != nil {
		panic(err)
	}

	return func(writer rest.ResponseWriter, request *rest.Request) {

		version, err := semver.NewVersion(request.PathParam("version"))
		if err != nil {
			rest.Error(writer, "Invalid version: "+err.Error(), http.StatusBadRequest)
			return
		}

		if version.LessThan(*minVersion) {
			rest.Error(writer, "Min supported version is "+minVersion.String(), http.StatusBadRequest)
			return
		}

		if maxVersion.LessThan(*version) {
			rest.Error(writer, "Max supported version is "+maxVersion.String(), http.StatusBadRequest)
			return
		}

		request.Env["VERSION"] = version
		handler(writer, request)
	}
}
Пример #8
0
func TestCheckStreamSupport(t *testing.T) {
	tests := []struct {
		v *semver.Version
		t streamType
		w bool
	}{
		// support
		{
			semver.Must(semver.NewVersion("2.1.0")),
			streamTypeMsgAppV2,
			true,
		},
		// ignore patch
		{
			semver.Must(semver.NewVersion("2.1.9")),
			streamTypeMsgAppV2,
			true,
		},
		// ignore prerelease
		{
			semver.Must(semver.NewVersion("2.1.0-alpha")),
			streamTypeMsgAppV2,
			true,
		},
	}
	for i, tt := range tests {
		if g := checkStreamSupport(tt.v, tt.t); g != tt.w {
			t.Errorf("#%d: check = %v, want %v", i, g, tt.w)
		}
	}
}
Пример #9
0
// ShouldUseIptablesProxier returns true if we should use the iptables Proxier
// instead of the "classic" userspace Proxier.  This is determined by checking
// the iptables version and for the existence of kernel features. It may return
// an error if it fails to get the itpables version without error, in which
// case it will also return false.
func ShouldUseIptablesProxier() (bool, error) {
	exec := utilexec.New()
	minVersion, err := semver.NewVersion(iptablesMinVersion)
	if err != nil {
		return false, err
	}
	// returns "X.X.X", err
	versionString, err := utiliptables.GetIptablesVersionString(exec)
	if err != nil {
		return false, err
	}
	version, err := semver.NewVersion(versionString)
	if err != nil {
		return false, err
	}
	if version.LessThan(*minVersion) {
		return false, nil
	}

	// Check for the required sysctls.  We don't care about the value, just
	// that it exists.  If this Proxier is chosen, we'll iniialize it as we
	// need.
	_, err = utilsysctl.GetSysctl(sysctlRouteLocalnet)
	if err != nil {
		return false, err
	}

	return true, nil
}
Пример #10
0
func TestDecideClusterVersion(t *testing.T) {
	tests := []struct {
		vers  map[string]*version.Versions
		wdver *semver.Version
	}{
		{
			map[string]*version.Versions{"a": {Server: "2.0.0"}},
			semver.Must(semver.NewVersion("2.0.0")),
		},
		// unknown
		{
			map[string]*version.Versions{"a": nil},
			nil,
		},
		{
			map[string]*version.Versions{"a": {Server: "2.0.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
			semver.Must(semver.NewVersion("2.0.0")),
		},
		{
			map[string]*version.Versions{"a": {Server: "2.1.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
			semver.Must(semver.NewVersion("2.1.0")),
		},
		{
			map[string]*version.Versions{"a": nil, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
			nil,
		},
	}

	for i, tt := range tests {
		dver := decideClusterVersion(tt.vers)
		if !reflect.DeepEqual(dver, tt.wdver) {
			t.Errorf("#%d: ver = %+v, want %+v", i, dver, tt.wdver)
		}
	}
}
Пример #11
0
// isCompatibleWithCluster return true if the local member has a compatible version with
// the current running cluster.
// The version is considered as compatible when at least one of the other members in the cluster has a
// cluster version in the range of [MinClusterVersion, Version] and no known members has a cluster version
// out of the range.
// We set this rule since when the local member joins, another member might be offline.
func isCompatibleWithCluster(cl Cluster, local types.ID, rt http.RoundTripper) bool {
	vers := getVersions(cl, local, rt)
	minV := semver.Must(semver.NewVersion(version.MinClusterVersion))
	maxV := semver.Must(semver.NewVersion(version.Version))
	maxV = &semver.Version{
		Major: maxV.Major,
		Minor: maxV.Minor,
	}

	return isCompatibleWithVers(vers, local, minV, maxV)
}
Пример #12
0
// checkVersionCompability checks whether the given version is compatible
// with the local version.
func checkVersionCompability(name string, server, minCluster *semver.Version) error {
	localServer := semver.Must(semver.NewVersion(version.Version))
	localMinCluster := semver.Must(semver.NewVersion(version.MinClusterVersion))
	if compareMajorMinorVersion(server, localMinCluster) == -1 {
		return fmt.Errorf("remote version is too low: remote[%s]=%s, local=%s", name, server, localServer)
	}
	if compareMajorMinorVersion(minCluster, localServer) == 1 {
		return fmt.Errorf("local version is too low: remote[%s]=%s, local=%s", name, server, localServer)
	}
	return nil
}
Пример #13
0
// GetUpdatePackage returns an update package for the instance/application
// provided. The instance details and the application it's running will be
// registered in CoreRoller (or updated if it's already registered).
func (api *API) GetUpdatePackage(instanceID, instanceIP, instanceVersion, appID, groupID string) (*Package, error) {
	instance, err := api.RegisterInstance(instanceID, instanceIP, instanceVersion, appID, groupID)
	if err != nil {
		return nil, ErrRegisterInstanceFailed
	}

	if instance.Application.Status.Valid {
		switch int(instance.Application.Status.Int64) {
		case InstanceStatusUpdateGranted, InstanceStatusDownloading, InstanceStatusDownloaded, InstanceStatusInstalled:
			return nil, ErrUpdateInProgressOnInstance
		}
	}

	group, err := api.GetGroup(groupID)
	if err != nil {
		return nil, err
	}

	if group.Channel == nil || group.Channel.Package == nil {
		_ = api.newGroupActivityEntry(activityPackageNotFound, activityWarning, "0.0.0", appID, groupID)
		return nil, ErrNoPackageFound
	}

	instanceSemver, _ := semver.NewVersion(instanceVersion)
	packageSemver, _ := semver.NewVersion(group.Channel.Package.Version)
	if !instanceSemver.LessThan(*packageSemver) {
		return nil, ErrNoUpdatePackageAvailable
	}

	updatesStats, err := api.getGroupUpdatesStats(group)
	if err != nil {
		return nil, ErrGetUpdatesStatsFailed
	}

	if err := api.enforceRolloutPolicy(instance, group, updatesStats); err != nil {
		return nil, err
	}

	if err := api.grantUpdate(instance.ID, appID, group.Channel.Package.Version); err != nil {
		return nil, ErrGrantingUpdate
	}

	if updatesStats.UpdatesToCurrentVersionGranted == 0 {
		_ = api.newGroupActivityEntry(activityRolloutStarted, activityInfo, group.Channel.Package.Version, appID, group.ID)
	}

	if !group.RolloutInProgress {
		_ = api.setGroupRolloutInProgress(groupID, true)
	}

	_ = api.updateInstanceStatus(instance.ID, appID, InstanceStatusUpdateGranted)

	return group.Channel.Package, nil
}
Пример #14
0
func main() {
	vA, err := semver.NewVersion(os.Args[1])
	if err != nil {
		fmt.Println(err.Error())
	}
	vB, err := semver.NewVersion(os.Args[2])
	if err != nil {
		fmt.Println(err.Error())
	}

	fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
}
Пример #15
0
func init() {
	sv, err := semver.NewVersion(strings.TrimPrefix(Version, "v"))
	if err != nil {
		panic(fmt.Sprintf("bad version string: %v", err))
	}
	SemVersion = *sv
}
Пример #16
0
func newRktVersion(version string) (rktVersion, error) {
	sem, err := semver.NewVersion(version)
	if err != nil {
		return rktVersion{}, err
	}
	return rktVersion{sem}, nil
}
Пример #17
0
func (a *applierV2store) Put(r *pb.Request) Response {
	ttlOptions := toTTLOptions(r)
	exists, existsSet := pbutil.GetBool(r.PrevExist)
	switch {
	case existsSet:
		if exists {
			if r.PrevIndex == 0 && r.PrevValue == "" {
				return toResponse(a.s.store.Update(r.Path, r.Val, ttlOptions))
			} else {
				return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
			}
		}
		return toResponse(a.s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions))
	case r.PrevIndex > 0 || r.PrevValue != "":
		return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
	default:
		if storeMemberAttributeRegexp.MatchString(r.Path) {
			id := membership.MustParseMemberIDFromKey(path.Dir(r.Path))
			var attr membership.Attributes
			if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
				plog.Panicf("unmarshal %s should never fail: %v", r.Val, err)
			}
			a.s.cluster.UpdateAttributes(id, attr)
			// return an empty response since there is no consumer.
			return Response{}
		}
		if r.Path == membership.StoreClusterVersionKey() {
			a.s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val)))
			// return an empty response since there is no consumer.
			return Response{}
		}
		return toResponse(a.s.store.Set(r.Path, r.Dir, r.Val, ttlOptions))
	}
}
Пример #18
0
// Checks if iptables has the "-C" flag
func getIptablesHasCheckCommand(vstring string) bool {
	minVersion, err := semver.NewVersion(MinCheckVersion)
	if err != nil {
		glog.Errorf("MinCheckVersion (%s) is not a valid version string: %v", MinCheckVersion, err)
		return true
	}
	version, err := semver.NewVersion(vstring)
	if err != nil {
		glog.Errorf("vstring (%s) is not a valid version string: %v", vstring, err)
		return true
	}
	if version.LessThan(*minVersion) {
		return false
	}
	return true
}
Пример #19
0
func isCompatibleWithVers(vers map[string]*version.Versions, local types.ID, minV, maxV *semver.Version) bool {
	var ok bool
	for id, v := range vers {
		// ignore comparison with local version
		if id == local.String() {
			continue
		}
		if v == nil {
			continue
		}
		clusterv, err := semver.NewVersion(v.Cluster)
		if err != nil {
			plog.Errorf("cannot understand the cluster version of member %s (%v)", id, err)
			continue
		}
		if clusterv.LessThan(*minV) {
			plog.Warningf("the running cluster version(%v) is lower than the minimal cluster version(%v) supported", clusterv.String(), minV.String())
			return false
		}
		if maxV.LessThan(*clusterv) {
			plog.Warningf("the running cluster version(%v) is higher than the maximum cluster version(%v) supported", clusterv.String(), maxV.String())
			return false
		}
		ok = true
	}
	return ok
}
Пример #20
0
// applyRequest interprets r as a call to store.X and returns a Response interpreted
// from store.Event
func (s *EtcdServer) applyRequest(r pb.Request) Response {
	f := func(ev *store.Event, err error) Response {
		return Response{Event: ev, err: err}
	}
	expr := timeutil.UnixNanoToTime(r.Expiration)
	refresh, _ := pbutil.GetBool(r.Refresh)
	ttlOptions := store.TTLOptionSet{ExpireTime: expr, Refresh: refresh}
	switch r.Method {
	case "POST":
		return f(s.store.Create(r.Path, r.Dir, r.Val, true, ttlOptions))
	case "PUT":
		exists, existsSet := pbutil.GetBool(r.PrevExist)
		switch {
		case existsSet:
			if exists {
				if r.PrevIndex == 0 && r.PrevValue == "" {
					return f(s.store.Update(r.Path, r.Val, ttlOptions))
				} else {
					return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
				}
			}
			return f(s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions))
		case r.PrevIndex > 0 || r.PrevValue != "":
			return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
		default:
			// TODO (yicheng): cluster should be the owner of cluster prefix store
			// we should not modify cluster store here.
			if storeMemberAttributeRegexp.MatchString(r.Path) {
				id := mustParseMemberIDFromKey(path.Dir(r.Path))
				var attr Attributes
				if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
					plog.Panicf("unmarshal %s should never fail: %v", r.Val, err)
				}
				ok := s.cluster.UpdateAttributes(id, attr)
				if !ok {
					return Response{}
				}
			}
			if r.Path == path.Join(StoreClusterPrefix, "version") {
				s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val)))
			}
			return f(s.store.Set(r.Path, r.Dir, r.Val, ttlOptions))
		}
	case "DELETE":
		switch {
		case r.PrevIndex > 0 || r.PrevValue != "":
			return f(s.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex))
		default:
			return f(s.store.Delete(r.Path, r.Dir, r.Recursive))
		}
	case "QGET":
		return f(s.store.Get(r.Path, r.Recursive, r.Sorted))
	case "SYNC":
		s.store.DeleteExpiredKeys(time.Unix(0, r.Time))
		return Response{}
	default:
		// This should never be reached, but just in case:
		return Response{err: ErrUnknownMethod}
	}
}
Пример #21
0
// serverVersion returns the min cluster version from the given header.
func minClusterVersion(h http.Header) *semver.Version {
	verStr := h.Get("X-Min-Cluster-Version")
	// backward compatibility with etcd 2.0
	if verStr == "" {
		verStr = "2.0.0"
	}
	return semver.Must(semver.NewVersion(verStr))
}
Пример #22
0
func version() string {
	version, err := semver.NewVersion(VERSION)
	if err != nil {
		errorMessage := fmt.Sprintf("Error with version number: %v", VERSION)
		log.Panicln(errorMessage, err.Error())
	}
	return version.String()
}
Пример #23
0
func TestCheckVersionCompatibility(t *testing.T) {
	ls := semver.Must(semver.NewVersion(version.Version))
	lmc := semver.Must(semver.NewVersion(version.MinClusterVersion))
	tests := []struct {
		server     *semver.Version
		minCluster *semver.Version
		wok        bool
	}{
		// the same version as local
		{
			ls,
			lmc,
			true,
		},
		// one version lower
		{
			lmc,
			&semver.Version{},
			true,
		},
		// one version higher
		{
			&semver.Version{Major: ls.Major + 1},
			ls,
			true,
		},
		// too low version
		{
			&semver.Version{Major: lmc.Major - 1},
			&semver.Version{},
			false,
		},
		// too high version
		{
			&semver.Version{Major: ls.Major + 1, Minor: 1},
			&semver.Version{Major: ls.Major + 1},
			false,
		},
	}
	for i, tt := range tests {
		err := checkVersionCompability("", tt.server, tt.minCluster)
		if ok := err == nil; ok != tt.wok {
			t.Errorf("#%d: ok = %v, want %v", i, ok, tt.wok)
		}
	}
}
Пример #24
0
func MustDetectDowngrade(cv *semver.Version) {
	lv := semver.Must(semver.NewVersion(version.Version))
	// only keep major.minor version for comparison against cluster version
	lv = &semver.Version{Major: lv.Major, Minor: lv.Minor}
	if cv != nil && lv.LessThan(*cv) {
		plog.Fatalf("cluster cannot be downgraded (current version: %s is lower than determined cluster version: %s).", version.Version, version.Cluster(cv.String()))
	}
}
Пример #25
0
func newFakeRegistryForCheckVersion(v string) registry.ClusterRegistry {
	sv, err := semver.NewVersion(v)
	if err != nil {
		panic(err)
	}

	return registry.NewFakeClusterRegistry(sv, 0)
}
Пример #26
0
func (c *RaftCluster) Version() *semver.Version {
	c.Lock()
	defer c.Unlock()
	if c.version == nil {
		return nil
	}
	return semver.Must(semver.NewVersion(c.version.String()))
}
Пример #27
0
// ShouldUseIptablesProxier returns true if we should use the iptables Proxier instead of
// the userspace Proxier.
// This is determined by the iptables version. It may return an erorr if it fails to get the
// itpables version without error, in which case it will also return false.
func ShouldUseIptablesProxier() (bool, error) {
	exec := utilexec.New()
	minVersion, err := semver.NewVersion(IPTABLES_MIN_VERSION)
	if err != nil {
		return false, err
	}
	// returns "vX.X.X", err
	versionString, err := utiliptables.GetIptablesVersionString(exec)
	if err != nil {
		return false, err
	}
	// make a semver of the part after the v in "vX.X.X"
	version, err := semver.NewVersion(versionString[1:])
	if err != nil {
		return false, err
	}
	return !version.LessThan(*minVersion), nil
}
Пример #28
0
func clusterVersionFromStore(st store.Store) *semver.Version {
	e, err := st.Get(path.Join(storePrefix, "version"), false, false)
	if err != nil {
		if isKeyNotFound(err) {
			return nil
		}
		plog.Panicf("unexpected error (%v) when getting cluster version from store", err)
	}
	return semver.Must(semver.NewVersion(*e.Node.Value))
}
Пример #29
0
func TestCompareMajorMinorVersion(t *testing.T) {
	tests := []struct {
		va, vb *semver.Version
		w      int
	}{
		// equal to
		{
			semver.Must(semver.NewVersion("2.1.0")),
			semver.Must(semver.NewVersion("2.1.0")),
			0,
		},
		// smaller than
		{
			semver.Must(semver.NewVersion("2.0.0")),
			semver.Must(semver.NewVersion("2.1.0")),
			-1,
		},
		// bigger than
		{
			semver.Must(semver.NewVersion("2.2.0")),
			semver.Must(semver.NewVersion("2.1.0")),
			1,
		},
		// ignore patch
		{
			semver.Must(semver.NewVersion("2.1.1")),
			semver.Must(semver.NewVersion("2.1.0")),
			0,
		},
		// ignore prerelease
		{
			semver.Must(semver.NewVersion("2.1.0-alpha.0")),
			semver.Must(semver.NewVersion("2.1.0")),
			0,
		},
	}
	for i, tt := range tests {
		if g := compareMajorMinorVersion(tt.va, tt.vb); g != tt.w {
			t.Errorf("#%d: compare = %d, want %d", i, g, tt.w)
		}
	}
}
Пример #30
0
// protocolVersionsAreCompatible checks that the two implementations
// can talk to each other. It will use semver, but for now while
// we're in tight development, we will return false for minor version
// changes too.
func protocolVersionsAreCompatible(v1, v2 string) bool {
	if strings.HasPrefix(v1, "ipfs/") {
		v1 = v1[5:]
	}
	if strings.HasPrefix(v2, "ipfs/") {
		v2 = v2[5:]
	}

	v1s, err := semver.NewVersion(v1)
	if err != nil {
		return false
	}

	v2s, err := semver.NewVersion(v2)
	if err != nil {
		return false
	}

	return v1s.Major == v2s.Major && v1s.Minor == v2s.Minor
}